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

347 lines
9.9 KiB
TypeScript

import type {CSSProperties, ReactNode} from 'react';
import {Link} from 'react-router';
import {Image} from '@shopify/hydrogen';
import {Instagram, Youtube} from 'lucide-react';
import type {FooterQuery, HeaderQuery} from 'storefrontapi.generated';
import {pickAbiirLogoData, type AbiirHeaderCms} from '~/components/Header';
import type {
AbiirFooterCms,
AbiirFooterColumn,
AbiirFooterLink,
} from '~/lib/footer';
import {MOCK_ABIIR_FOOTER_CMS} from '~/lib/footer';
/**
* GROQ: `settings` singleton → ABiiR footer module.
* Use with `context.sanity.loadQuery(FOOTER_SECTION_GROQ)` or `loadFooterFromSanity`.
*/
export const FOOTER_SECTION_GROQ = `*[_type == "settings" && _id == "settings"][0].abiirFooter{
tagline,
brandLinkPath,
emblem{
alt,
asset->{
url,
metadata{dimensions{width,height}}
}
},
wordmark{
alt,
asset->{
url,
metadata{dimensions{width,height}}
}
},
certificationBadges{
alt,
asset->{
url,
metadata{dimensions{width,height}}
}
},
socialInstagramUrl,
socialYoutubeUrl,
socialXUrl,
columnGroups[]{
title,
links[]{ label, path }
},
copyrightLine,
legalLinks[]{ label, path }
}` as const;
export {MOCK_ABIIR_FOOTER_CMS};
const CREAM = '#faf8f3';
const LINK_MUTED = 'rgba(250, 248, 243, 0.8)';
const BODY_MUTED = 'rgba(250, 248, 243, 0.7)';
const LEGAL_MUTED = 'rgba(250, 248, 243, 0.6)';
const BORDER_TOP = 'rgba(250, 248, 243, 0.15)';
function SocialX({className, size}: {className?: string; size?: number}) {
const s = size ?? 19;
return (
<svg
aria-hidden
className={className}
fill="none"
height={s}
viewBox="0 0 24 24"
width={s}
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M13.522 10.774 20.48 3h-1.646l-6.04 6.748L8.158 3H3l7.304 10.12L3 21h1.646l6.388-7.142L15.842 21H21l-7.478-10.226ZM11.66 13.02l-.732-1.01L5.22 4.2h2.513l4.71 6.488.732 1.01 6.11 8.42h-2.514l-4.99-6.878Z"
fill="currentColor"
/>
</svg>
);
}
function isExternalHref(href: string) {
return /^https?:\/\//i.test(href);
}
function FooterNavLink({
href,
className,
style,
children,
}: {
href: string;
className?: string;
style?: CSSProperties;
children: ReactNode;
}) {
if (isExternalHref(href)) {
return (
<a
className={className}
href={href}
rel="noopener noreferrer"
style={style}
target="_blank"
>
{children}
</a>
);
}
return (
<Link className={className} style={style} to={href}>
{children}
</Link>
);
}
function isRemoteImageUrl(url: string) {
return /^https?:\/\//i.test(url);
}
function FooterRemoteImage({
url,
alt,
width,
height,
className,
style,
sizes,
}: {
url: string;
alt: string;
width: number;
height: number;
className?: string;
style?: CSSProperties;
sizes?: string;
}) {
if (isRemoteImageUrl(url)) {
return (
<Image
alt={alt}
className={className}
data={{
url,
altText: alt,
width,
height,
}}
loader={({src}) => src ?? ''}
sizes={sizes}
style={style}
/>
);
}
return (
<Image
alt={alt}
className={className}
height={height}
sizes={sizes}
src={url}
style={style}
width={width}
/>
);
}
const linkClass =
'font-["Raleway",sans-serif] text-[15.938px] font-light leading-[23.906px] transition-opacity hover:opacity-80 no-underline';
const linkStyle: CSSProperties = {color: LINK_MUTED};
const columnHeadingClass =
'm-0 font-["Raleway",sans-serif] text-[11.688px] font-semibold leading-[15.778px] tracking-[2.5712px] uppercase';
const columnHeadingStyle: CSSProperties = {color: CREAM};
const legalLinkClass =
'font-["Raleway",sans-serif] text-[13.813px] font-light leading-[20.719px] tracking-[0.5525px] transition-opacity hover:opacity-60 whitespace-nowrap no-underline';
const legalLinkStyle: CSSProperties = {color: LEGAL_MUTED};
function FooterNavColumn({column}: {column: AbiirFooterColumn}) {
return (
<div
className="flex min-w-0 flex-col"
style={{gap: '29.75px'}}
>
<h4 className={columnHeadingClass} style={columnHeadingStyle}>
{column.title}
</h4>
<ul className="m-0 flex list-none flex-col p-0" style={{gap: '17px'}}>
{column.links.map((link: AbiirFooterLink) => (
<li key={link.label}>
<FooterNavLink
className={linkClass}
href={link.href}
style={linkStyle}
>
{link.label}
</FooterNavLink>
</li>
))}
</ul>
</div>
);
}
export type AbiirFooterProps = {
/** Shopify menu query (optional; reserved for future use). */
footer: FooterQuery | null;
footerCms: AbiirFooterCms;
header: HeaderQuery;
/** Same source as `Header` — drives footer brand mark (`pickAbiirLogoData`). */
headerCms: AbiirHeaderCms;
publicStoreDomain: string;
};
export function Footer({
footer: _footer,
footerCms,
header,
headerCms,
publicStoreDomain: _publicStoreDomain,
}: AbiirFooterProps) {
const cms = footerCms;
const logoData = pickAbiirLogoData(headerCms, header.shop.brand?.logo?.image);
const columnGroups = cms.columnGroups;
const firstNavPair = columnGroups.slice(0, 2);
const restNavColumns = columnGroups.slice(2);
return (
<footer className="w-full" style={{background: '#1a1a18', color: CREAM}}>
<div className="abiir-footer-main mx-auto max-w-[1710px] px-6 pt-[72px] sm:px-10 sm:pt-[88px] lg:px-16 lg:pt-[102px] 2xl:px-[206px]">
<div className="mx-auto grid max-w-[1298px] grid-cols-1 gap-x-10 gap-y-12 lg:grid-cols-[minmax(0,493.695px)_repeat(3,minmax(0,225.602px))] lg:gap-x-[42.5px] lg:gap-y-0">
{/* Brand */}
<div className="lg:max-w-[493.695px]">
<FooterNavLink
aria-label="ABiiR — Home"
className="mb-5 inline-flex items-center transition-opacity hover:opacity-90"
href={cms.brandLinkPath}
style={{textDecoration: 'none'}}
>
<Image
alt={logoData.altText || 'ABiiR Skincare'}
className="block h-auto w-[148px] max-w-[148px]"
data={logoData}
sizes="148px"
width={148}
/>
</FooterNavLink>
<p
className="m-0 max-w-[340px] font-['Raleway',sans-serif] text-[15.938px] font-light leading-[30.281px]"
style={{color: BODY_MUTED}}
>
{cms.tagline}
</p>
<div
className="mt-8 flex items-center"
style={{gap: '21.25px'}}
>
<a
aria-label="Instagram"
className="flex h-[19px] w-[19px] items-center justify-center transition-opacity hover:opacity-60"
href={cms.socialInstagramUrl}
rel="noopener noreferrer"
style={{color: CREAM}}
target="_blank"
>
<Instagram aria-hidden size={19} strokeWidth={1.5} />
</a>
<a
aria-label="YouTube"
className="flex h-[19px] w-[19px] items-center justify-center transition-opacity hover:opacity-60"
href={cms.socialYoutubeUrl}
rel="noopener noreferrer"
style={{color: CREAM}}
target="_blank"
>
<Youtube aria-hidden size={19} strokeWidth={1.5} />
</a>
<a
aria-label="X"
className="flex h-[19px] w-[19px] items-center justify-center transition-opacity hover:opacity-60"
href={cms.socialXUrl}
rel="noopener noreferrer"
style={{color: CREAM}}
target="_blank"
>
<SocialX size={19} />
</a>
</div>
<div className="mt-8 min-w-0 max-w-full">
<FooterRemoteImage
alt={cms.certificationBadgesAlt}
className="block h-auto w-full max-w-[320px] object-contain opacity-90"
height={cms.certificationBadgesHeight ?? 80}
sizes="(max-width: 1024px) min(100vw, 520px), 320px"
url={cms.certificationBadgesUrl}
width={cms.certificationBadgesWidth ?? 320}
/>
</div>
</div>
{firstNavPair.length >= 2 ? (
<div className="grid min-w-0 grid-cols-2 gap-x-6 gap-y-0 sm:gap-x-10 lg:contents">
{firstNavPair.map((column) => (
<FooterNavColumn key={column.title} column={column} />
))}
</div>
) : (
firstNavPair.map((column) => (
<FooterNavColumn key={column.title} column={column} />
))
)}
{restNavColumns.map((column) => (
<FooterNavColumn key={column.title} column={column} />
))}
</div>
</div>
<div
className="abiir-footer-legal mx-auto max-w-[1400px] px-6 sm:px-10 lg:px-[51px]"
style={{borderTop: `1px solid ${BORDER_TOP}`}}
>
<p
className="m-0 text-center font-['Raleway',sans-serif] text-[13.813px] font-light leading-[24.862px] tracking-[0.5525px] sm:text-left"
style={{color: LEGAL_MUTED}}
>
{cms.copyrightLine}
</p>
<div
className="flex flex-wrap items-center justify-center sm:justify-end"
style={{gap: '29.75px'}}
>
{cms.legalLinks.map((item) => (
<FooterNavLink
key={item.label}
className={legalLinkClass}
href={item.href}
style={legalLinkStyle}
>
{item.label}
</FooterNavLink>
))}
</div>
</div>
</footer>
);
}