280 lines
8.8 KiB
TypeScript
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>
|
|
);
|
|
}
|