AbiirSkincare/Hydrogen/web/app/components/JournalPage.tsx

394 lines
14 KiB
TypeScript

import {useState} from 'react';
import {Image} from '@shopify/hydrogen';
import {Link} from 'react-router';
import {motion} from 'motion/react';
import type {CSSProperties} from 'react';
import type {AbiirJournalIndexCms, AbiirJournalPostCms} from '~/lib/journal';
import {abiirJournalImageIsRemote, MOCK_ABIIR_JOURNAL_INDEX} from '~/lib/journal';
import type {NewsletterSectionCms} from '~/lib/newsletter';
import {Newsletter} from '~/components/Newsletter';
import {PAGE_TITLE_TYPOGRAPHY_CLASS} from '~/lib/pageTitle';
import iconClockFeatured from '~/assets/journal/icon-clock-featured.svg';
import iconClockGrid from '~/assets/journal/icon-clock-grid.svg';
import iconReadArrow from '~/assets/journal/icon-read-arrow.svg';
/**
* Bundle: journal index singleton + all posts.
* Use with `context.sanity.loadQuery(JOURNAL_PAGE_GROQ)` or `useQuery`.
*/
export const JOURNAL_PAGE_GROQ = `{
"index": *[_type == "journalIndex" && _id == "journalIndex"][0]{
seo{ title, description },
eyebrow,
headline,
description,
categoryFilters
},
"posts": *[_type == "journalPost"] | order(_updatedAt desc) {
"slug": slug.current,
title,
subtitle,
excerpt,
cardExcerpt,
content,
author,
authorRole,
date,
readTime,
category,
featured,
tags,
image{
alt,
asset->{
_id,
url,
metadata{dimensions{width,height}}
}
}
}
}` as const;
export {MOCK_ABIIR_JOURNAL_INDEX};
export type {AbiirJournalIndexCms, AbiirJournalPostCms};
const CHARCOAL = '#1a1a18';
const ACCENT = '#c17d5c';
const MUTED = '#5a5550';
const MUTED_MID = '#7a7068';
const CREAM_TEXT = '#f8f4ef';
const HERO_BG = '#ede8e0';
const BORDER_HAIRLINE = 'rgba(26,26,24,0.08)';
const BORDER_BTN = 'rgba(26,26,24,0.15)';
const DOT_MUTED = 'rgba(26,26,24,0.15)';
function gridCardTeaser(post: AbiirJournalPostCms) {
if (post.cardExcerpt) return post.cardExcerpt;
const t = post.excerpt.trim();
return t.length > 100 ? `${t.slice(0, 97).trim()}...` : t;
}
/**
* Hydrogen `Image` with numeric `width`/`height` uses FixedWidthImage and fixed `<img>` dimensions.
* Pairing that with `absolute inset-0` broke rendering. Use fluid images (`width="100%"`) + `aspectRatio`.
*/
function JournalPostImage({
url,
alt,
className,
sizes,
style,
aspectRatio,
}: {
url: string;
alt: string;
className?: string;
sizes: string;
style?: CSSProperties;
aspectRatio: '4/3' | '8/5';
}) {
const imgClass = `block size-full object-cover ${className ?? ''}`;
if (abiirJournalImageIsRemote(url)) {
return (
<Image
alt={alt}
aspectRatio={aspectRatio}
className={imgClass}
data={{
url,
altText: alt,
width: 1200,
height: 800,
}}
height="auto"
loader={({src}) => src ?? ''}
sizes={sizes}
style={style}
width="100%"
/>
);
}
return (
<Image
alt={alt}
aspectRatio={aspectRatio}
className={imgClass}
height="auto"
sizes={sizes}
src={url}
style={style}
width="100%"
/>
);
}
export type JournalPageProps = {
indexCms: AbiirJournalIndexCms;
postsCms: AbiirJournalPostCms[];
newsletterCms: NewsletterSectionCms;
};
export function JournalPage({indexCms, postsCms, newsletterCms}: JournalPageProps) {
const [activeCategory, setActiveCategory] = useState('All');
const blogCategories = indexCms.categoryFilters;
const filtered =
activeCategory === 'All'
? postsCms
: postsCms.filter((p) => p.category === activeCategory);
const featured = postsCms.filter((p) => p.featured);
const rest = filtered.filter(
(p) => !p.featured || activeCategory !== 'All',
);
return (
<div className="min-h-screen bg-[#f8f4ef]">
<header className="w-full bg-[#ede8e0]">
<div className="mx-auto max-w-[1710px] px-6 sm:px-10 lg:px-16 2xl:px-[206px]">
<motion.div
animate={{opacity: 1, y: 0}}
className="mx-auto max-w-[1298px] pb-10 pt-[57px] text-center sm:pb-12"
initial={{opacity: 0, y: 20}}
style={{minHeight: 'min(258px, auto)'}}
transition={{duration: 0.55}}
>
<p
className="m-0 font-['Jost',sans-serif] text-[11.05px] font-medium uppercase leading-[16.575px] tracking-[2.7625px] not-italic"
style={{color: ACCENT}}
>
{indexCms.eyebrow}
</p>
<h1
className={`m-0 mt-[17.5px] text-[#1a1a18] ${PAGE_TITLE_TYPOGRAPHY_CLASS}`}
>
{indexCms.headline}
</h1>
<p
className="m-0 mx-auto mt-5 max-w-[720px] font-['Raleway',sans-serif] text-[16px] font-medium leading-[33.022px] not-italic"
style={{color: MUTED}}
>
{indexCms.description}
</p>
</motion.div>
</div>
</header>
{activeCategory === 'All' && (
<section className="w-full pt-10 lg:pt-[60px]">
<div className="mx-auto max-w-[1710px] px-6 sm:px-10 lg:px-16 2xl:px-[206px]">
<div className="mx-auto grid max-w-[1298px] grid-cols-1 gap-8 lg:grid-cols-2 lg:gap-[34px]">
{featured.map((post, i) => (
<motion.article
key={post.id}
animate={{opacity: 1, y: 0}}
initial={{opacity: 0, y: 24}}
transition={{duration: 0.55, delay: i * 0.08}}
>
<Link
className="group block text-inherit no-underline"
prefetch="intent"
to={`/journal/${post.id}`}
>
<div
className="relative aspect-[8/5] w-full overflow-hidden sm:aspect-[16/10]"
style={{background: HERO_BG}}
>
<JournalPostImage
alt=""
aspectRatio="8/5"
className="transition-transform duration-700 group-hover:scale-[1.03]"
sizes="(min-width: 1024px) 50vw, 100vw"
url={post.image}
/>
</div>
<div className="mt-[21px] flex flex-wrap items-center gap-[17px]">
<span
className="inline-flex shrink-0 items-center font-['Jost',sans-serif] text-[9.35px] font-medium uppercase leading-[14.025px] tracking-[1.4025px] not-italic"
style={{
background: CHARCOAL,
color: CREAM_TEXT,
padding: '4.25px 12.75px',
}}
>
{post.category}
</span>
<div className="flex items-center gap-[6.375px]">
<Image
alt=""
className="size-[11px] shrink-0"
height={11}
src={iconClockFeatured}
width={11}
/>
<span
className="font-['Jost',sans-serif] text-[14px] font-normal leading-[16.575px] not-italic"
style={{color: MUTED_MID}}
>
{post.readTime}
</span>
</div>
</div>
<h3
className="m-0 mt-[13px] font-['Cormorant_Garamond',serif] text-[32px] font-normal leading-[40.375px] tracking-[-0.4845px] not-italic"
style={{color: CHARCOAL}}
>
{post.title}
</h3>
<p
className="m-0 mt-2 max-w-[613px] font-['Cormorant_Garamond',serif] text-[18px] font-medium leading-normal not-italic"
style={{color: CHARCOAL}}
>
{post.excerpt}
</p>
</Link>
</motion.article>
))}
</div>
</div>
</section>
)}
<div className="mx-auto max-w-[1710px] px-6 pt-10 sm:px-10 lg:px-16 lg:pt-14 2xl:px-[206px]">
<div
className="mx-auto flex max-w-[1298px] flex-wrap items-center gap-[8.5px] pb-0"
style={{
borderBottom: `1px solid ${BORDER_HAIRLINE}`,
minHeight: '69.805px',
}}
>
{blogCategories.map((cat) => {
const active = activeCategory === cat;
return (
<button
key={cat}
className="cursor-pointer transition-colors duration-200"
style={{
background: active ? CHARCOAL : 'transparent',
color: active ? CREAM_TEXT : MUTED_MID,
fontFamily: "'Jost', sans-serif",
fontSize: '10.54px',
fontWeight: 500,
lineHeight: '15.81px',
letterSpacing: '1.2648px',
textTransform: 'uppercase',
border: `1px solid ${BORDER_BTN}`,
padding: '9.5px 18px',
}}
type="button"
onClick={() => setActiveCategory(cat)}
>
{cat}
</button>
);
})}
</div>
</div>
<section className="mx-auto max-w-[1710px] px-6 pb-20 pt-10 sm:px-10 lg:px-16 lg:pt-12 2xl:px-[206px]">
<div className="mx-auto grid w-full max-w-[1298px] grid-cols-1 gap-x-[34px] gap-y-10 md:grid-cols-3 md:gap-y-[34px]">
{(activeCategory === 'All' ? rest : filtered).map((post, i) => (
<motion.article
key={post.id}
className="min-w-0"
initial={{opacity: 0, y: 20}}
transition={{duration: 0.5, delay: Math.min(i * 0.06, 0.36)}}
viewport={{once: true}}
whileInView={{opacity: 1, y: 0}}
>
<Link
className="group block text-inherit no-underline"
prefetch="intent"
to={`/journal/${post.id}`}
>
<div
className="relative aspect-[4/3] w-full overflow-hidden"
style={{background: HERO_BG}}
>
<JournalPostImage
alt=""
aspectRatio="4/3"
className="transition-transform duration-700 group-hover:scale-[1.03]"
sizes="(min-width: 768px) 33vw, 100vw"
url={post.image}
/>
</div>
<div className="mt-[17px] flex flex-wrap items-center gap-[12.75px]">
<span
className="font-['Jost',sans-serif] text-[12px] font-medium uppercase leading-[14.025px] tracking-[1.4025px] not-italic"
style={{color: ACCENT}}
>
{post.category}
</span>
<span
className="select-none font-['Raleway',sans-serif] text-[10.2px] font-normal leading-[15.3px] not-italic"
style={{color: DOT_MUTED}}
>
·
</span>
<div className="flex items-center gap-[4.25px]">
<Image
alt=""
className="size-[10px] shrink-0"
height={10}
src={iconClockGrid}
width={10}
/>
<span
className="font-['Jost',sans-serif] text-[14px] font-normal leading-[16.575px] not-italic"
style={{color: MUTED_MID}}
>
{post.readTime}
</span>
</div>
</div>
<h3
className="m-0 mt-[13px] font-['Cormorant_Garamond',serif] text-[24px] font-medium leading-[24.48px] tracking-[-0.204px] not-italic"
style={{color: CHARCOAL}}
>
{post.title}
</h3>
<p
className="m-0 mt-2 min-h-[3.5rem] font-['Cormorant_Garamond',serif] text-[18px] font-medium leading-normal not-italic"
style={{color: CHARCOAL}}
>
{gridCardTeaser(post)}
</p>
<div className="mt-6 flex items-center justify-between">
<span
className="font-['Jost',sans-serif] text-[14px] font-normal leading-[16.575px] not-italic"
style={{color: MUTED_MID}}
>
{post.date}
</span>
<span
className="inline-flex shrink-0 items-center gap-1.5 font-['Jost',sans-serif] text-[10.54px] font-medium uppercase leading-[15.81px] tracking-[1.054px] not-italic"
style={{color: CHARCOAL}}
>
Read More
<Image
alt=""
className="size-[11px] shrink-0"
height={11}
src={iconReadArrow}
width={11}
/>
</span>
</div>
</Link>
</motion.article>
))}
</div>
</section>
<Newsletter cms={newsletterCms} />
</div>
);
}