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

280 lines
8.8 KiB
TypeScript

import {Link} from 'react-router';
import {Image} from '@shopify/hydrogen';
import {motion} from 'motion/react';
import {
Beaker,
Droplets,
Flame,
Leaf,
PillBottle,
type LucideIcon,
} from 'lucide-react';
import type {
IngredientsIconKey,
IngredientsSectionCms,
} from '~/lib/ingredients';
import {MOCK_INGREDIENTS_SECTION_CMS} from '~/lib/ingredients';
import {PAGE_TITLE_TYPOGRAPHY_CLASS} from '~/lib/pageTitle';
/** Same file as `React/src/assets/fff9b697e083c70c2aa6491b8a1715c65334534.png` (copied). */
import ingredientsHeroFallbackSrc from '~/assets/ingredients-hero.png';
/**
* GROQ: `home` singleton → ingredients philosophy module.
* Use with `context.sanity.loadQuery(INGREDIENTS_SECTION_GROQ)` or `useQuery` in the browser.
*/
export const INGREDIENTS_SECTION_GROQ = `*[_type == "home" && _id == "home"][0].ingredientsSection{
eyebrow,
headlineLine1Before,
headlineLine1Accent,
headlineLine2,
body,
badgePercent,
badgeCaption,
primaryCtaLabel,
primaryCtaHref,
sectionBackground,
gold,
cream,
creamMuted,
badgeBackground,
charcoal,
image{
alt,
asset->{
_id,
url,
metadata{dimensions{width,height}}
}
},
ingredients[]{
iconKey,
name,
description
}
}` as const;
export {MOCK_INGREDIENTS_SECTION_CMS};
export type {IngredientsSectionCms};
const ICONS: Record<IngredientsIconKey, LucideIcon> = {
leaf: Leaf,
flame: Flame,
pillBottle: PillBottle,
droplets: Droplets,
beaker: Beaker,
};
function isExternalHref(href: string) {
return /^https?:\/\//i.test(href);
}
function IngredientsHeroImage({cms}: {cms: IngredientsSectionCms}) {
const alt =
cms.image?.alt ?? 'Ayurvedic ingredients and ritual';
if (cms.image?.url) {
const w = cms.image.width ?? 1200;
const h = cms.image.height ?? 900;
return (
<Image
alt={alt}
className="block h-auto max-w-full w-full object-contain"
data={{
url: cms.image.url,
altText: alt,
width: w,
height: h,
}}
loader={({src}) => src ?? ''}
sizes="(min-width: 1024px) 525px, 100vw"
/>
);
}
return (
<Image
alt={alt}
className="block h-auto max-w-full w-full object-contain"
height={812}
sizes="(min-width: 1024px) 525px, 100vw"
src={ingredientsHeroFallbackSrc}
width={525}
/>
);
}
function PrimaryCta({
href,
background,
children,
}: {
href: string;
background: string;
children: React.ReactNode;
}) {
const className =
'inline-flex min-h-[51.844px] cursor-pointer items-center justify-center px-[34px] py-[17px] font-["Raleway",sans-serif] text-[14px] font-bold uppercase leading-[19.89px] tracking-[2.3868px] text-white no-underline transition-opacity duration-200 box-border hover:opacity-90';
const style = {
boxSizing: 'border-box' as const,
background,
};
if (isExternalHref(href)) {
return (
<a href={href} className={className} style={style}>
{children}
</a>
);
}
return (
<Link to={href} className={className} style={style}>
{children}
</Link>
);
}
export type IngredientsProps = {
cms: IngredientsSectionCms;
};
/**
* `React/src/app/components/Ingredients.tsx` — Tailwind + Hydrogen `Image`, CMS-driven.
*/
export function Ingredients({cms}: IngredientsProps) {
const bg = cms.sectionBackground;
const gold = cms.gold;
const cream = cms.cream;
const creamMuted = cms.creamMuted;
const badgeBg = cms.badgeBackground;
const charcoal = cms.charcoal;
return (
<section
className="w-full overflow-x-clip overflow-y-visible"
style={{background: bg}}
>
<div className="mx-auto max-w-[1710px] px-6 pt-16 pb-16 sm:px-10 sm:pt-20 sm:pb-20 lg:px-16 lg:pt-[94px] lg:pb-[94px] xl:px-24 2xl:px-[206px]">
<div className="flex flex-col gap-6 lg:flex-row lg:items-stretch lg:justify-between lg:gap-10 xl:gap-16">
<motion.div
initial={false}
whileInView={{opacity: 1, x: 0}}
viewport={{once: true, margin: '-60px'}}
transition={{duration: 0.75, ease: [0.25, 0.1, 0.25, 1]}}
className="relative w-full shrink-0 overflow-visible lg:w-[min(100%,525px)] lg:max-w-[525px]"
>
<div className="relative w-full overflow-hidden">
<IngredientsHeroImage cms={cms} />
</div>
<div
className="abiir-ingredients-badge absolute top-4 right-[-10px] z-[1] text-left lg:top-[41.62px]"
style={{
background: badgeBg,
padding: '10px 25px',
}}
>
<p
className="abiir-ingredients-badge-percent"
style={{color: charcoal,fontSize:'2rem'}}
>
{cms.badgePercent}
</p>
<p
className="abiir-ingredients-badge-caption"
style={{color: charcoal}}
>
{cms.badgeCaption}
</p>
</div>
</motion.div>
<div className="flex w-full flex-col gap-5 lg:max-w-[635px] lg:justify-center lg:gap-10">
<div className="flex flex-col gap-4 lg:gap-6">
<div className="flex flex-col gap-2 lg:gap-4">
<p
className="m-0 font-['Jost',sans-serif] text-[12px] font-medium uppercase leading-[16.575px] tracking-[2.7625px] not-italic"
style={{color: gold}}
>
{cms.eyebrow}
</p>
<h2
className={`m-0 ${PAGE_TITLE_TYPOGRAPHY_CLASS}`}
style={{color: cream}}
>
<span className="block">
{cms.headlineLine1Before}
{cms.headlineLine1Before && (cms.headlineLine1Accent || cms.headlineLine2)
? ' '
: ''}
{cms.headlineLine1Accent ? (
<span
style={{fontStyle: 'italic', fontWeight: 300, color: gold}}
>
{cms.headlineLine1Accent}
</span>
) : null}
{cms.headlineLine1Accent && cms.headlineLine2 ? ' ' : ''}
{cms.headlineLine2}
</span>
</h2>
</div>
<p
className="m-0 font-['Raleway',sans-serif] text-[16px] font-medium leading-[26.18px] not-italic"
style={{color: cream}}
>
{cms.body}
</p>
<ul className="m-0 flex list-none flex-col gap-6 p-0">
{cms.ingredients.map(({name, description, iconKey}, index) => {
const Icon = ICONS[iconKey];
return (
<motion.li
key={`${name}-${index}`}
initial={false}
whileInView={{opacity: 1, y: 0}}
viewport={{once: true, margin: '-40px'}}
transition={{duration: 0.5, delay: 0.05 * index}}
className="flex items-start gap-4 pb-2 pr-4"
>
<span className="mt-0.5 shrink-0" style={{color: gold}}>
<Icon size={22} strokeWidth={1.35} aria-hidden />
</span>
<div className="flex min-w-0 flex-col gap-1">
<h3
className="m-0 font-['Cormorant_Garamond',serif] text-[24px] font-medium tracking-[-0.187px] not-italic"
style={{color: cream}}
>
{name}
</h3>
<p
className="m-0 overflow-hidden text-ellipsis whitespace-nowrap font-['Raleway',sans-serif] text-[16px] font-medium leading-[23.698px] not-italic"
style={{color: creamMuted}}
title={description}
>
{description}
</p>
</div>
</motion.li>
);
})}
</ul>
</div>
<motion.div
initial={false}
whileInView={{opacity: 1, y: 0}}
viewport={{once: true}}
transition={{duration: 0.5, delay: 0.2}}
>
<PrimaryCta href={cms.primaryCtaHref} background={gold}>
{cms.primaryCtaLabel}
</PrimaryCta>
</motion.div>
</div>
</div>
</div>
</section>
);
}