394 lines
14 KiB
TypeScript
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>
|
|
);
|
|
}
|