import {Image} from '@shopify/hydrogen'; import {Link} from 'react-router'; import {motion} from 'motion/react'; import {ArrowLeft, Clock, Tag} from 'lucide-react'; import {MOCK_ABIIR_JOURNAL_POSTS} from '~/lib/journal'; import type {AbiirJournalPostCms} from '~/lib/journal'; import {abiirJournalImageIsRemote} from '~/lib/journal'; import {PAGE_CONTENT_MAX_WIDTH_CLASS} from '~/lib/pageTitle'; import {PAGE_TITLE_TYPOGRAPHY_CLASS} from '~/lib/pageTitle'; // ─── Sanity: GROQ projection (single `journalPost` document) ───────────────── /** * GROQ field projection for one journal article. Reused by `buildJournalPostGroq`. * Sanity schema: `journalPost` in `Hydrogen/studio` (title, slug, content, image, etc.). */ export const JOURNAL_POST_GROQ_PROJECTION = /* groq */ `{ "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; /** * Builds a **parameterized** GROQ query for a single post by `slug` (JSON-stringified, safe for GROQ). * Use in the route `loader` with `context.sanity.loadQuery(buildJournalPostGroq(params.slug))`. */ export function buildJournalPostGroq(slug: string): string { const safe = JSON.stringify(slug); return `*[_type == "journalPost" && slug.current == ${safe}][0]${JOURNAL_POST_GROQ_PROJECTION}`; } /** * Static **example** query (illustrates shape only). Prefer `buildJournalPostGroq` in production. */ export const JOURNAL_POST_PAGE_GROQ_EXAMPLE = `*[_type == "journalPost" && slug.current == "ancient-art-ayurvedic-skincare"][0]${JOURNAL_POST_GROQ_PROJECTION}` as const; // ─── Mock: visual / Storybook (matches `React` blog post shape) ───────────── function buildMockRelated(post: AbiirJournalPostCms): AbiirJournalPostCms[] { return MOCK_ABIIR_JOURNAL_POSTS.filter( (p) => p.id !== post.id && p.category === post.category, ).slice(0, 3); } /** `JournalPostPage` with first seed post + related — for local preview or design QA. */ export const MOCK_JOURNAL_POST_PAGE_PROPS: { post: AbiirJournalPostCms; relatedPosts: AbiirJournalPostCms[]; } = { post: MOCK_ABIIR_JOURNAL_POSTS[0]!, relatedPosts: buildMockRelated(MOCK_ABIIR_JOURNAL_POSTS[0]!), }; // ─── Images: Shopify Hydrogen (local Vite + Sanity CDN) ─────────── function HeroImage({url, alt}: {url: string; alt: string}) { if (abiirJournalImageIsRemote(url)) { return ( {alt} src ?? ''} sizes="100vw" width="100%" /> ); } return ( {alt} ); } function RelatedThumb({url, alt}: {url: string; alt: string}) { const imgClass = 'block h-full w-full min-h-0 object-cover transition-transform duration-500 group-hover:scale-105'; if (abiirJournalImageIsRemote(url)) { return ( {alt} src ?? ''} sizes="(min-width: 640px) 33vw, 100vw" width="100%" /> ); } return ( {alt} ); } // ─── Page: blog detail (1:1 with `React/src/app/pages/JournalPost.tsx`) ───── export type JournalPostPageProps = { /** Single article (from GROQ / loader) or mock. */ post: AbiirJournalPostCms; relatedPosts: AbiirJournalPostCms[]; }; export function JournalPostPage({post, relatedPosts}: JournalPostPageProps) { const paragraphs = post.content .split('\n\n') .map((p) => p.trim()) .filter(Boolean); return (
{/* Breadcrumb — Figma: px-6 py-4, border 6% black */}
Journal / {post.category}
{/* Hero: 55vh, min 360px (reference) */}
{/* Category & meta — gap-4, mb-6 = 16px / 24px */}
{post.category}
{post.readTime}
{post.date}
{post.title}

{post.subtitle}

{/* Author — border-bottom, gap-3, padding bottom 8 */}
{post.author .split(' ') .map((n) => n[0]) .join('')}

{post.author}

{post.authorRole}

{/* Content — space-y-5 = 20px; body Jost 0.92rem /1.9; Cormorant “bold” lines 1.15rem/1.5 */}
{paragraphs.map((para, i) => { const isBold = para.startsWith('**') && para.includes('**'); if (isBold) { const boldText = para.replace(/\*\*(.*?)\*\*/g, '$1'); return (

{boldText}

); } return (

{para}

); })}
{/* Tags — border-top, pt-10 mt-10 */}
Tags:
{post.tags.map((tag) => ( {tag} ))}
{/* Continue reading — #EDE8E0, py-16 / lg:py-20, max 1400 */} {relatedPosts.length > 0 && (
Continue Reading
{relatedPosts.map((relPost, i) => (
{relPost.category}

{relPost.title}

Read →
))}
)}
); } export function JournalPostNotFound() { return (

Article not found

Back to Journal
); }