990 lines
36 KiB
TypeScript
990 lines
36 KiB
TypeScript
import type {QueryResponseInitial} from '@sanity/react-loader';
|
||
import type {SanityContext} from 'hydrogen-sanity';
|
||
import {unwrapSanityQueryResult} from './abiirProductDetail.ts';
|
||
import type {AbiirLegalPageCms, AbiirLegalSectionCms} from './abiirLegalPageTypes.ts';
|
||
import {
|
||
MOCK_PRIVACY_POLICY_PAGE,
|
||
MOCK_TERMS_OF_SERVICE_PAGE,
|
||
} from './abiirLegalPageMocks.ts';
|
||
|
||
export type {AbiirLegalPageCms, AbiirLegalSectionCms} from './abiirLegalPageTypes.ts';
|
||
|
||
// ─── GROQ (singleton document IDs match Studio) ────────────────────────────
|
||
|
||
export const ABIIR_FAQ_PAGE_GROQ = `*[_type == "abiirFaqPage" && _id == "abiirFaqPage"][0]{
|
||
title,
|
||
subtitle,
|
||
items[]{ question, answer },
|
||
sections[]{
|
||
sectionTitle,
|
||
items[]{ question, answer }
|
||
},
|
||
seo{ title, description }
|
||
}` as const;
|
||
|
||
export const ABIIR_SHIPPING_PAGE_GROQ = `*[_type == "abiirShippingPage" && _id == "abiirShippingPage"][0]{
|
||
title, intro, paragraphs,
|
||
shippingOptions[]{ title, leadTime, priceLine, detail },
|
||
returnsPolicy[]{ title, body },
|
||
bannerHeadline, supportEmail, bannerTextBeforeEmail, bannerTextAfterEmail,
|
||
seo{ title, description }
|
||
}` as const;
|
||
|
||
export const ABIIR_TRACK_PAGE_GROQ = `*[_type == "abiirTrackOrderPage" && _id == "abiirTrackOrderPage"][0]{
|
||
title, intro, paragraphs, seo{ title, description }
|
||
}` as const;
|
||
|
||
export const ABIIR_QUIZ_PAGE_GROQ = `*[_type == "abiirSkincareQuizPage" && _id == "abiirSkincareQuizPage"][0]{
|
||
title, intro, paragraphs, seo{ title, description }
|
||
}` as const;
|
||
|
||
export const ABIIR_CONTACT_PAGE_GROQ = `*[_type == "abiirContactPage" && _id == "abiirContactPage"][0]{
|
||
title, intro, email, phone, address, businessHours, mapEmbedUrl, showContactForm, formHeadline, formSuccessText,
|
||
howToReachHeading, followUsLabel, heroEyebrow,
|
||
reachRows[]{ label, primary, note, icon },
|
||
socialLinks[]{ label, href, network },
|
||
formPlaceholders{ name, email, subject, message },
|
||
formSubmitLabel,
|
||
seo{ title, description }
|
||
}` as const;
|
||
|
||
export const ABIIR_PRESS_PAGE_GROQ = `*[_type == "abiirPressPage" && _id == "abiirPressPage"][0]{
|
||
title, intro,
|
||
items[]{
|
||
title, listCaption, modalDescription,
|
||
image{ alt, asset->{ _id, url, metadata{ dimensions{ width, height } } } }
|
||
},
|
||
seo{ title, description }
|
||
}` as const;
|
||
|
||
export const ABIIR_PRIVACY_POLICY_PAGE_GROQ = `*[_type == "abiirPrivacyPolicyPage" && _id == "abiirPrivacyPolicyPage"][0]{
|
||
title, heroEyebrow, lastUpdatedLine, lead,
|
||
sections[]{ title, paragraphs, listItems, trailingParagraphs },
|
||
seo{ title, description }
|
||
}` as const;
|
||
|
||
export const ABIIR_TERMS_OF_SERVICE_PAGE_GROQ = `*[_type == "abiirTermsOfServicePage" && _id == "abiirTermsOfServicePage"][0]{
|
||
title, heroEyebrow, lastUpdatedLine, lead,
|
||
sections[]{ title, paragraphs, listItems, trailingParagraphs },
|
||
seo{ title, description }
|
||
}` as const;
|
||
|
||
// ─── Types + mocks ───────────────────────────────────────────────────────────
|
||
|
||
export type AbiirFaqItem = {question: string; answer: string};
|
||
|
||
export type AbiirFaqSection = {sectionTitle: string; items: AbiirFaqItem[]};
|
||
|
||
export type AbiirFaqPageCms = {
|
||
title: string;
|
||
subtitle: string;
|
||
sections: AbiirFaqSection[];
|
||
seoTitle: string;
|
||
seoDescription: string;
|
||
};
|
||
|
||
export const MOCK_FAQ_CMS: AbiirFaqPageCms = {
|
||
title: 'Frequently Asked Questions',
|
||
subtitle:
|
||
'Everything you need to know about ABiiR, our products, orders, shipping, and returns.',
|
||
sections: [
|
||
{
|
||
sectionTitle: 'Products & Ingredients',
|
||
items: [
|
||
{
|
||
question: 'Are ABiiR products suitable for sensitive skin?',
|
||
answer:
|
||
'Yes, all ABiiR formulas are dermatologist-tested and free from common irritants including synthetic fragrances, parabens, sulfates, and artificial dyes. However, we always recommend performing a patch test when trying a new product, especially if you have known sensitivities.',
|
||
},
|
||
{
|
||
question: 'Are your products vegan and cruelty-free?',
|
||
answer:
|
||
'Yes! All products in our collection are cruelty-free — and we never test on animals.',
|
||
},
|
||
{
|
||
question: 'What makes ABiiR different from other skincare brands?',
|
||
answer:
|
||
'ABiiR sits at the intersection of ancient Ayurvedic wisdom and modern cosmetic science. Every formula starts with time-honored botanical ingredients — many sourced directly from Ayurvedic tradition — then integrates clinically validated actives for measurable results. We also prioritize complete ingredient transparency.',
|
||
},
|
||
{
|
||
question: 'Can I use multiple ABiiR products together?',
|
||
answer:
|
||
'Absolutely — our products are formulated to work synergistically. We recommend our Skin Ritual guide for layering recommendations. As a general rule: cleanse, tone, treat (serum), then moisturize, finishing with SPF in the morning.',
|
||
},
|
||
{
|
||
question: 'How long before I see results?',
|
||
answer:
|
||
'Most customers notice improved hydration and texture within the first week. For results like reduced hyperpigmentation or fine lines, allow 4–6 weeks of consistent use. Skin renewal is a gradual process, and our formulas are designed for long-term transformation, not quick fixes.',
|
||
},
|
||
],
|
||
},
|
||
{
|
||
sectionTitle: 'Orders & Shipping',
|
||
items: [
|
||
{
|
||
question: 'How long does shipping take?',
|
||
answer:
|
||
'Standard shipping within the US takes 3–5 business days. Express shipping (1–2 business days) is available at checkout. Currently, we do not offer international shipping. Please check back in the future, as we hope to expand our shipping options!',
|
||
},
|
||
{
|
||
question: 'Do you offer free shipping?',
|
||
answer:
|
||
'Yes! All US orders over $75 qualify for complimentary standard shipping. Use code ABiiR15 at checkout for 15% off your first order.',
|
||
},
|
||
{
|
||
question: 'Can I track my order?',
|
||
answer:
|
||
"Once your order ships, you'll receive a confirmation email with a tracking number. You can track your package directly on our website or through the carrier's website.",
|
||
},
|
||
{
|
||
question: 'Can I make any changes to my order?',
|
||
answer:
|
||
'We are unable to change or cancel any orders once they are placed. Be sure to check your order details and shipping information before placing your order.',
|
||
},
|
||
{
|
||
question: 'Do you ship internationally?',
|
||
answer:
|
||
'Currently, we do not offer international shipping. We apologize for any inconvenience this may cause and appreciate your understanding. Please check back in the future, as we hope to expand our shipping options!',
|
||
},
|
||
],
|
||
},
|
||
{
|
||
sectionTitle: 'Returns & Refunds',
|
||
items: [
|
||
{
|
||
question: 'What is your return policy?',
|
||
answer:
|
||
"We offer free returns within 15 days of purchase for all unopened products in their original packaging. If a product doesn't work for your skin, contact us at abiir.skincare@gmail.com — we'll work with you to find the right solution.",
|
||
},
|
||
{
|
||
question: 'How do I initiate a return?',
|
||
answer:
|
||
'Email abiir.skincare@gmail.com with your order number and reason for return. Our team will respond within 24 hours with a prepaid return label and instructions.',
|
||
},
|
||
{
|
||
question: 'When will I receive my refund?',
|
||
answer:
|
||
"Refunds are processed within 5–7 business days of receiving your returned item. You'll receive an email confirmation once the refund has been issued. Please allow an additional 3–5 business days for the credit to appear on your statement.",
|
||
},
|
||
{
|
||
question: 'What should I do if my order arrives damaged or defective?',
|
||
answer:
|
||
"If your order arrives damaged or defective, please email abiir.skincare@gmail.com with a detailed description and clear photos within 48 hours of delivery. Claims for damaged, missing, or incorrect items must be submitted within 15 days of the delivery date. We'll be happy to replace any eligible items right away.",
|
||
},
|
||
{
|
||
question: "What's not eligible for return?",
|
||
answer:
|
||
'Experience kits (mini size) are final sale and cannot be returned or exchanged. Free gifts during Gift with Purchase promotions are final sale and are not eligible for return or exchange. The Gua Sha Facial Tool is final sale for hygiene reasons and is not eligible for refunds or exchanges.',
|
||
},
|
||
],
|
||
},
|
||
],
|
||
seoTitle: 'Frequently Asked Questions | ABiiR Skincare',
|
||
seoDescription:
|
||
'Products and ingredients, US shipping and tracking, returns and refunds — answers in one place.',
|
||
};
|
||
|
||
export type AbiirInfoPageCms = {
|
||
title: string;
|
||
intro: string;
|
||
paragraphs: string[];
|
||
seoTitle: string;
|
||
seoDescription: string;
|
||
};
|
||
|
||
export type AbiirShippingOptionCms = {
|
||
title: string;
|
||
leadTime: string;
|
||
priceLine: string;
|
||
detail: string;
|
||
};
|
||
|
||
export type AbiirShippingReturnCms = {
|
||
title: string;
|
||
body: string;
|
||
};
|
||
|
||
export type AbiirShippingPageCms = {
|
||
title: string;
|
||
intro: string;
|
||
shippingOptions: AbiirShippingOptionCms[];
|
||
returnsPolicy: AbiirShippingReturnCms[];
|
||
bannerHeadline: string;
|
||
supportEmail: string;
|
||
bannerTextBeforeEmail: string;
|
||
bannerTextAfterEmail: string;
|
||
seoTitle: string;
|
||
seoDescription: string;
|
||
};
|
||
|
||
export const MOCK_SHIPPING_PAGE: AbiirShippingPageCms = {
|
||
title: 'Shipping & Returns',
|
||
intro: '',
|
||
shippingOptions: [
|
||
{
|
||
title: 'How long does shipping take?',
|
||
leadTime: 'Standard & express',
|
||
priceLine: 'US domestic',
|
||
detail:
|
||
'Standard shipping within the US takes 3–5 business days. Express shipping (1–2 business days) is available at checkout. Currently, we do not offer international shipping. Please check back in the future, as we hope to expand our shipping options!',
|
||
},
|
||
{
|
||
title: 'Do you offer free shipping?',
|
||
leadTime: 'Complimentary shipping',
|
||
priceLine: 'Orders $75+ · ABiiR15',
|
||
detail:
|
||
'Yes! All US orders over $75 qualify for complimentary standard shipping. Use code ABiiR15 at checkout for 15% off your first order.',
|
||
},
|
||
{
|
||
title: 'Can I track my order?',
|
||
leadTime: 'After it ships',
|
||
priceLine: 'Email & tracking',
|
||
detail:
|
||
"Once your order ships, you'll receive a confirmation email with a tracking number. You can track your package directly on our website or through the carrier's website.",
|
||
},
|
||
{
|
||
title: 'Can I make any changes to my order?',
|
||
leadTime: 'Order changes',
|
||
priceLine: 'Final at checkout',
|
||
detail:
|
||
'We are unable to change or cancel any orders once they are placed. Be sure to check your order details and shipping information before placing your order.',
|
||
},
|
||
{
|
||
title: 'Do you ship internationally?',
|
||
leadTime: 'International',
|
||
priceLine: 'Not at this time',
|
||
detail:
|
||
'Currently, we do not offer international shipping. We apologize for any inconvenience this may cause and appreciate your understanding. Please check back in the future, as we hope to expand our shipping options!',
|
||
},
|
||
],
|
||
returnsPolicy: [
|
||
{
|
||
title: 'What is your return policy?',
|
||
body: "We offer free returns within 15 days of purchase for all unopened products in their original packaging. If a product doesn't work for your skin, contact us at hello@abiirskincare.com — we'll work with you to find the right solution.",
|
||
},
|
||
{
|
||
title: 'How do I initiate a return?',
|
||
body: 'Email hello@abiirskincare.com with your order number and reason for return. Our team will respond within 24 hours with a prepaid return label and instructions.',
|
||
},
|
||
{
|
||
title: 'When will I receive my refund?',
|
||
body: "Refunds are processed within 5–7 business days of receiving your returned item. You'll receive an email confirmation once the refund has been issued. Please allow an additional 3–5 business days for the credit to appear on your statement.",
|
||
},
|
||
{
|
||
title: 'What should I do if my order arrives damaged or defective?',
|
||
body: "If your order arrives damaged or defective, please email hello@abiirskincare.com with a detailed description and clear photos within 48 hours of delivery. Claims for damaged, missing, or incorrect items must be submitted within 15 days of the delivery date. We'll be happy to replace any eligible items right away.",
|
||
},
|
||
{
|
||
title: "What's not eligible for return?",
|
||
body: 'Experience kits (mini size) are final sale and cannot be returned or exchanged.\n\nFree gifts during Gift with Purchase promotions are final sale and are not eligible for return or exchange.\n\nThe Gua Sha Facial Tool is final sale for hygiene reasons and is not eligible for refunds or exchanges.',
|
||
},
|
||
],
|
||
bannerHeadline: 'Have a question not covered here?',
|
||
supportEmail: 'abiirskincare@gmail.com',
|
||
bannerTextBeforeEmail: 'Email us at ',
|
||
bannerTextAfterEmail: ' — we respond within 24 hours.',
|
||
seoTitle: 'Shipping & Returns | ABiiR Skincare',
|
||
seoDescription:
|
||
'US shipping timelines, free shipping over $75, tracking, returns within 15 days, and how to reach support.',
|
||
};
|
||
|
||
/** Storefront defaults for `/track-order` (Studio: `abiirTrackOrderPage`). */
|
||
export const MOCK_TRACK_ORDER_PAGE: AbiirInfoPageCms = {
|
||
title: 'Track your order',
|
||
intro: 'Find and follow your package.',
|
||
paragraphs: [
|
||
'When your order ships, you will receive an email with a tracking link. You can also log in to your account and view order status for recent purchases.',
|
||
'If tracking has not updated for several days, the carrier may be experiencing delays. Reach out with your order number and we can help.',
|
||
'For pre-orders or bundles, items may ship together once all pieces are in stock.',
|
||
],
|
||
seoTitle: 'Track my order | ABiiR Skincare',
|
||
seoDescription: 'How to find tracking information for your order.',
|
||
};
|
||
|
||
/** Storefront defaults for `/skincare-quiz` (Studio: `abiirSkincareQuizPage`). */
|
||
export const MOCK_SKINCARE_QUIZ_PAGE: AbiirInfoPageCms = {
|
||
title: 'Skincare Quiz',
|
||
intro: 'Seven quick questions—then personalized product picks for your skin.',
|
||
paragraphs: [
|
||
'Not sure where to start? The ritual that fits you depends on your goals—hydration, brightening, barrier repair, and daily consistency all play a role in healthy-looking skin.',
|
||
'Consider your current routine: are you looking for a simple morning refresh, a weekly treatment, or a full ritual? Our collections are designed to work alone or in layers, so you can build slowly.',
|
||
'For sensitive skin, patch test new products and introduce one formula at a time. For questions about ingredients or pairings, our team can recommend options that align with your skin type and lifestyle.',
|
||
],
|
||
seoTitle: 'Skincare Quiz | ABiiR Skincare',
|
||
seoDescription:
|
||
'Interactive skincare quiz: skin type, concerns, routine, and more—see tailored ABiiR picks.',
|
||
};
|
||
|
||
export type AbiirContactReachRowIcon = 'mail' | 'message' | 'clock';
|
||
|
||
export type AbiirContactReachRowCms = {
|
||
label: string;
|
||
primary: string;
|
||
note: string;
|
||
icon: AbiirContactReachRowIcon;
|
||
};
|
||
|
||
export type AbiirContactSocialNetwork = 'instagram' | 'youtube';
|
||
|
||
export type AbiirContactSocialCms = {
|
||
label: string;
|
||
href: string;
|
||
network: AbiirContactSocialNetwork;
|
||
};
|
||
|
||
export type AbiirContactFormPlaceholdersCms = {
|
||
name: string;
|
||
email: string;
|
||
subject: string;
|
||
message: string;
|
||
};
|
||
|
||
export type AbiirContactPageCms = {
|
||
title: string;
|
||
intro: string;
|
||
/** “Get in Touch” in Figma */
|
||
heroEyebrow: string;
|
||
email: string;
|
||
phone: string;
|
||
address: string;
|
||
businessHours: string;
|
||
mapEmbedUrl: string;
|
||
showContactForm: boolean;
|
||
howToReachHeading: string;
|
||
followUsLabel: string;
|
||
reachRows: AbiirContactReachRowCms[];
|
||
socialLinks: AbiirContactSocialCms[];
|
||
formPlaceholders: AbiirContactFormPlaceholdersCms;
|
||
formSubmitLabel: string;
|
||
formHeadline: string;
|
||
formSuccessText: string;
|
||
seoTitle: string;
|
||
seoDescription: string;
|
||
};
|
||
|
||
export const MOCK_CONTACT_CMS: AbiirContactPageCms = {
|
||
title: 'Contact Us',
|
||
heroEyebrow: 'Get in Touch',
|
||
intro: "We'd love to hear from you. Our team responds within 24 hours.",
|
||
email: 'hello@abiirskincare.com',
|
||
phone: '',
|
||
address: '',
|
||
businessHours: '',
|
||
mapEmbedUrl: '',
|
||
showContactForm: true,
|
||
howToReachHeading: 'How to Reach Us',
|
||
followUsLabel: 'Follow Us',
|
||
reachRows: [
|
||
{
|
||
label: 'Email',
|
||
primary: 'hello@abiirskincare.com',
|
||
note: 'General inquiries & orders',
|
||
icon: 'mail',
|
||
},
|
||
{
|
||
label: 'Live Chat',
|
||
primary: 'Chat with us on the website',
|
||
note: 'Available Mon–Fri, 9am–6pm EST',
|
||
icon: 'message',
|
||
},
|
||
{
|
||
label: 'Response Time',
|
||
primary: 'Within 24 hours',
|
||
note: 'Typically much faster on weekdays',
|
||
icon: 'clock',
|
||
},
|
||
],
|
||
socialLinks: [
|
||
{
|
||
label: '@abiir_skincare',
|
||
href: 'https://www.instagram.com/abiir_skincare/',
|
||
network: 'instagram',
|
||
},
|
||
{
|
||
label: 'ABiiR Skincare',
|
||
href: 'https://www.youtube.com/',
|
||
network: 'youtube',
|
||
},
|
||
],
|
||
formPlaceholders: {
|
||
name: 'Jane Smith',
|
||
email: 'jane@example.com',
|
||
subject: 'Order inquiry, product question...',
|
||
message: 'How can we help you today?',
|
||
},
|
||
formSubmitLabel: 'Send message',
|
||
formHeadline: '',
|
||
formSuccessText: 'Thank you. We have received your message and will respond shortly.',
|
||
seoTitle: 'Contact | ABiiR Skincare',
|
||
seoDescription:
|
||
'Get in touch with ABiiR: email, live chat hours, and our contact form.',
|
||
};
|
||
|
||
export type AbiirPressImageCms = {
|
||
url: string;
|
||
alt: string;
|
||
width?: number;
|
||
height?: number;
|
||
};
|
||
|
||
export type AbiirPressItemCms = {
|
||
title: string;
|
||
listCaption: string;
|
||
modalDescription: string;
|
||
image: AbiirPressImageCms | null;
|
||
};
|
||
|
||
export type AbiirPressPageCms = {
|
||
title: string;
|
||
intro: string;
|
||
items: AbiirPressItemCms[];
|
||
seoTitle: string;
|
||
seoDescription: string;
|
||
};
|
||
|
||
export const MOCK_PRESS_CMS: AbiirPressPageCms = {
|
||
title: 'Press',
|
||
intro: 'Selected features, launches, and stories. Click an item to read more.',
|
||
items: [
|
||
{
|
||
title: 'Modern Ritual: The Launch Profile',
|
||
listCaption: 'Summer 2025',
|
||
modalDescription:
|
||
"A profile of ABiiR's approach to small-batch rituals and the founders' vision for conscious skincare. The feature highlights the Brightening line and the Experience Kit as entry points to Ayurvedic inspired care.",
|
||
image: null,
|
||
},
|
||
{
|
||
title: "Editor’s pick: The bundle we can't quit",
|
||
listCaption: 'In print — Issue 12',
|
||
modalDescription:
|
||
"Editors call out the Glow With Love bundle as a thoughtful gift and a complete four-step routine for first-time customers exploring the line.",
|
||
image: null,
|
||
},
|
||
{
|
||
title: "San Jose founder on building beauty with intention",
|
||
listCaption: 'Local business spotlight',
|
||
modalDescription:
|
||
"An interview on staying independent, sourcing botanicals, and what “honest skincare” means in formulation — plus what is next for the brand in 2026.",
|
||
image: null,
|
||
},
|
||
],
|
||
seoTitle: 'Press | ABiiR Skincare',
|
||
seoDescription: 'Press coverage, features, and brand stories.',
|
||
};
|
||
|
||
// ─── Normalize ──────────────────────────────────────────────────────────────
|
||
|
||
function cleanParas(raw: Array<string | null | undefined> | null | undefined): string[] {
|
||
if (!raw?.length) return [];
|
||
return raw.map((p) => (typeof p === 'string' ? p.trim() : '')).filter(Boolean);
|
||
}
|
||
|
||
function normSeo(
|
||
s: {title?: string | null; description?: string | null} | null | undefined,
|
||
fallback: {t: string; d: string},
|
||
) {
|
||
return {
|
||
seoTitle: s?.title?.trim() || fallback.t,
|
||
seoDescription: s?.description?.trim() || fallback.d,
|
||
};
|
||
}
|
||
|
||
function normPressImage(
|
||
field: {
|
||
alt?: string | null;
|
||
asset?: {
|
||
_id?: string;
|
||
url?: string | null;
|
||
metadata?: {dimensions?: {width?: number | null; height?: number | null}} | null;
|
||
} | null;
|
||
} | null
|
||
| undefined,
|
||
): AbiirPressImageCms | null {
|
||
const u = field?.asset?.url?.trim();
|
||
if (!u) return null;
|
||
return {
|
||
url: u,
|
||
alt: typeof field?.alt === 'string' ? field.alt : '',
|
||
width: field?.asset?.metadata?.dimensions?.width ?? undefined,
|
||
height: field?.asset?.metadata?.dimensions?.height ?? undefined,
|
||
};
|
||
}
|
||
|
||
function normFaqItems(
|
||
rows: Array<{question?: string | null; answer?: string | null} | null> | null | undefined,
|
||
): AbiirFaqItem[] {
|
||
const out: AbiirFaqItem[] = [];
|
||
for (const row of rows ?? []) {
|
||
const q = row?.question?.trim();
|
||
const a = row?.answer?.trim();
|
||
if (q && a) out.push({question: q, answer: a});
|
||
}
|
||
return out;
|
||
}
|
||
|
||
export function normalizeFaqPayload(raw: unknown): AbiirFaqPageCms | null {
|
||
if (raw == null || typeof raw !== 'object') return null;
|
||
const d = raw as {
|
||
title?: string | null;
|
||
subtitle?: string | null;
|
||
items?: Array<{question?: string | null; answer?: string | null} | null> | null;
|
||
sections?: Array<{
|
||
sectionTitle?: string | null;
|
||
items?: Array<{question?: string | null; answer?: string | null} | null> | null;
|
||
} | null> | null;
|
||
seo?: {title?: string | null; description?: string | null} | null;
|
||
};
|
||
const title = d.title?.trim();
|
||
if (!title) return null;
|
||
|
||
let sections: AbiirFaqSection[] = [];
|
||
for (const block of d.sections ?? []) {
|
||
if (!block) continue;
|
||
const sectionTitle = block.sectionTitle?.trim();
|
||
const items = normFaqItems(block.items);
|
||
if (sectionTitle && items.length) sections.push({sectionTitle, items});
|
||
}
|
||
|
||
if (!sections.length) {
|
||
const legacy = normFaqItems(d.items);
|
||
if (legacy.length) {
|
||
sections = [{sectionTitle: 'General', items: legacy}];
|
||
}
|
||
}
|
||
|
||
if (!sections.length) return null;
|
||
|
||
const {seoTitle, seoDescription} = normSeo(d.seo, {
|
||
t: MOCK_FAQ_CMS.seoTitle,
|
||
d: MOCK_FAQ_CMS.seoDescription,
|
||
});
|
||
return {
|
||
title,
|
||
subtitle: d.subtitle?.trim() || MOCK_FAQ_CMS.subtitle,
|
||
sections,
|
||
seoTitle,
|
||
seoDescription,
|
||
};
|
||
}
|
||
|
||
function normalizeInfoPayload(
|
||
raw: unknown,
|
||
mock: AbiirInfoPageCms,
|
||
): AbiirInfoPageCms | null {
|
||
if (raw == null || typeof raw !== 'object') return null;
|
||
const d = raw as {
|
||
title?: string | null;
|
||
intro?: string | null;
|
||
paragraphs?: Array<string | null> | null;
|
||
seo?: {title?: string | null; description?: string | null} | null;
|
||
};
|
||
const title = d.title?.trim();
|
||
if (!title) return null;
|
||
const paragraphs = cleanParas(d.paragraphs ?? []);
|
||
const intro = (d.intro?.trim() || mock.intro) ?? '';
|
||
if (!paragraphs.length && !intro.trim()) return null;
|
||
const {seoTitle, seoDescription} = normSeo(d.seo, {t: mock.seoTitle, d: mock.seoDescription});
|
||
return {
|
||
title,
|
||
intro: intro || mock.intro,
|
||
paragraphs,
|
||
seoTitle,
|
||
seoDescription,
|
||
};
|
||
}
|
||
|
||
function normShippingOptions(
|
||
raw:
|
||
| Array<{
|
||
title?: string | null;
|
||
leadTime?: string | null;
|
||
priceLine?: string | null;
|
||
detail?: string | null;
|
||
} | null>
|
||
| null
|
||
| undefined,
|
||
): AbiirShippingOptionCms[] {
|
||
const out: AbiirShippingOptionCms[] = [];
|
||
for (const row of raw ?? []) {
|
||
if (!row) continue;
|
||
const title = row.title?.trim();
|
||
const leadTime = row.leadTime?.trim();
|
||
const priceLine = row.priceLine?.trim();
|
||
const detail = row.detail?.trim();
|
||
if (title && leadTime && priceLine && detail) {
|
||
out.push({title, leadTime, priceLine, detail});
|
||
}
|
||
}
|
||
return out;
|
||
}
|
||
|
||
function normReturnsPolicy(
|
||
raw:
|
||
| Array<{
|
||
title?: string | null;
|
||
body?: string | null;
|
||
} | null>
|
||
| null
|
||
| undefined,
|
||
): AbiirShippingReturnCms[] {
|
||
const out: AbiirShippingReturnCms[] = [];
|
||
for (const row of raw ?? []) {
|
||
if (!row) continue;
|
||
const title = row.title?.trim();
|
||
const body = row.body?.trim();
|
||
if (title && body) out.push({title, body});
|
||
}
|
||
return out;
|
||
}
|
||
|
||
export function normalizeShippingPagePayload(raw: unknown): AbiirShippingPageCms {
|
||
const base = MOCK_SHIPPING_PAGE;
|
||
if (raw == null || typeof raw !== 'object') return base;
|
||
const d = raw as {
|
||
title?: string | null;
|
||
intro?: string | null;
|
||
shippingOptions?: Array<{
|
||
title?: string | null;
|
||
leadTime?: string | null;
|
||
priceLine?: string | null;
|
||
detail?: string | null;
|
||
} | null> | null;
|
||
returnsPolicy?: Array<{
|
||
title?: string | null;
|
||
body?: string | null;
|
||
} | null> | null;
|
||
bannerHeadline?: string | null;
|
||
supportEmail?: string | null;
|
||
bannerTextBeforeEmail?: string | null;
|
||
bannerTextAfterEmail?: string | null;
|
||
seo?: {title?: string | null; description?: string | null} | null;
|
||
};
|
||
const title = d.title?.trim() || base.title;
|
||
const intro = d.intro?.trim() ?? base.intro;
|
||
const opts = normShippingOptions(d.shippingOptions);
|
||
const ret = normReturnsPolicy(d.returnsPolicy);
|
||
const {seoTitle, seoDescription} = normSeo(d.seo, {
|
||
t: base.seoTitle,
|
||
d: base.seoDescription,
|
||
});
|
||
return {
|
||
title,
|
||
intro,
|
||
shippingOptions: opts.length ? opts : base.shippingOptions,
|
||
returnsPolicy: ret.length ? ret : base.returnsPolicy,
|
||
bannerHeadline: d.bannerHeadline?.trim() || base.bannerHeadline,
|
||
supportEmail: d.supportEmail?.trim() || base.supportEmail,
|
||
bannerTextBeforeEmail: d.bannerTextBeforeEmail?.trim() || base.bannerTextBeforeEmail,
|
||
bannerTextAfterEmail: d.bannerTextAfterEmail?.trim() || base.bannerTextAfterEmail,
|
||
seoTitle,
|
||
seoDescription,
|
||
};
|
||
}
|
||
|
||
function normContactReachRows(
|
||
raw: Array<{
|
||
label?: string | null;
|
||
primary?: string | null;
|
||
note?: string | null;
|
||
icon?: string | null;
|
||
} | null> | null | undefined,
|
||
email: string,
|
||
): AbiirContactReachRowCms[] {
|
||
const out: AbiirContactReachRowCms[] = [];
|
||
const icons: AbiirContactReachRowIcon[] = ['mail', 'message', 'clock'];
|
||
for (let i = 0; i < (raw?.length ?? 0); i++) {
|
||
const row = raw?.[i];
|
||
if (!row) continue;
|
||
const label = row.label?.trim();
|
||
const primary = row.primary?.trim();
|
||
const note = row.note?.trim();
|
||
const iconRaw = row.icon?.trim().toLowerCase();
|
||
const icon: AbiirContactReachRowIcon =
|
||
iconRaw === 'mail' || iconRaw === 'message' || iconRaw === 'clock'
|
||
? iconRaw
|
||
: icons[i % 3]!;
|
||
const primaryVal =
|
||
label?.toLowerCase() === 'email'
|
||
? (primary?.trim() || email)
|
||
: primary?.trim();
|
||
const noteVal = note?.trim();
|
||
if (label && primaryVal && noteVal) {
|
||
out.push({
|
||
label,
|
||
primary: primaryVal,
|
||
note: noteVal,
|
||
icon,
|
||
});
|
||
}
|
||
}
|
||
return out;
|
||
}
|
||
|
||
function normContactSocial(
|
||
raw: Array<{
|
||
label?: string | null;
|
||
href?: string | null;
|
||
network?: string | null;
|
||
} | null> | null | undefined,
|
||
): AbiirContactSocialCms[] {
|
||
const out: AbiirContactSocialCms[] = [];
|
||
for (const row of raw ?? []) {
|
||
if (!row) continue;
|
||
const label = row.label?.trim();
|
||
const href = row.href?.trim();
|
||
const n = row.network?.toLowerCase();
|
||
const network: AbiirContactSocialNetwork =
|
||
n === 'instagram' || n === 'youtube' ? n : 'instagram';
|
||
if (label && href) out.push({label, href, network});
|
||
}
|
||
return out;
|
||
}
|
||
|
||
export function normalizeContactPayload(raw: unknown): AbiirContactPageCms | null {
|
||
if (raw == null || typeof raw !== 'object') return null;
|
||
const d = raw as {
|
||
title?: string | null;
|
||
intro?: string | null;
|
||
email?: string | null;
|
||
phone?: string | null;
|
||
address?: string | null;
|
||
businessHours?: string | null;
|
||
mapEmbedUrl?: string | null;
|
||
showContactForm?: boolean | null;
|
||
formHeadline?: string | null;
|
||
formSuccessText?: string | null;
|
||
heroEyebrow?: string | null;
|
||
howToReachHeading?: string | null;
|
||
followUsLabel?: string | null;
|
||
reachRows?: Array<{
|
||
label?: string | null;
|
||
primary?: string | null;
|
||
note?: string | null;
|
||
icon?: string | null;
|
||
} | null> | null;
|
||
socialLinks?: Array<{
|
||
label?: string | null;
|
||
href?: string | null;
|
||
network?: string | null;
|
||
} | null> | null;
|
||
formPlaceholders?: {
|
||
name?: string | null;
|
||
email?: string | null;
|
||
subject?: string | null;
|
||
message?: string | null;
|
||
} | null;
|
||
formSubmitLabel?: string | null;
|
||
seo?: {title?: string | null; description?: string | null} | null;
|
||
};
|
||
const title = d.title?.trim();
|
||
const email = d.email?.trim();
|
||
if (!title || !email) return null;
|
||
const m = MOCK_CONTACT_CMS;
|
||
const {seoTitle, seoDescription} = normSeo(d.seo, {
|
||
t: m.seoTitle,
|
||
d: m.seoDescription,
|
||
});
|
||
const reachRows = normContactReachRows(d.reachRows, email);
|
||
const socialLinks = normContactSocial(d.socialLinks);
|
||
const fp = d.formPlaceholders;
|
||
return {
|
||
title,
|
||
intro: d.intro?.trim() || m.intro,
|
||
heroEyebrow: d.heroEyebrow?.trim() || m.heroEyebrow,
|
||
email,
|
||
phone: d.phone?.trim() || '',
|
||
address: d.address?.trim() || '',
|
||
businessHours: d.businessHours?.trim() || '',
|
||
mapEmbedUrl: d.mapEmbedUrl?.trim() || '',
|
||
showContactForm: d.showContactForm !== false,
|
||
howToReachHeading: d.howToReachHeading?.trim() || m.howToReachHeading,
|
||
followUsLabel: d.followUsLabel?.trim() || m.followUsLabel,
|
||
reachRows: reachRows.length
|
||
? reachRows
|
||
: m.reachRows.map((r) => ({
|
||
...r,
|
||
primary: r.label.toLowerCase() === 'email' ? email : r.primary,
|
||
})),
|
||
socialLinks: socialLinks.length ? socialLinks : m.socialLinks,
|
||
formPlaceholders: {
|
||
name: fp?.name?.trim() || m.formPlaceholders.name,
|
||
email: fp?.email?.trim() || m.formPlaceholders.email,
|
||
subject: fp?.subject?.trim() || m.formPlaceholders.subject,
|
||
message: fp?.message?.trim() || m.formPlaceholders.message,
|
||
},
|
||
formSubmitLabel: d.formSubmitLabel?.trim() || m.formSubmitLabel,
|
||
formHeadline: d.formHeadline?.trim() || m.formHeadline,
|
||
formSuccessText: d.formSuccessText?.trim() || m.formSuccessText,
|
||
seoTitle,
|
||
seoDescription,
|
||
};
|
||
}
|
||
|
||
export function normalizePressPayload(raw: unknown): AbiirPressPageCms | null {
|
||
if (raw == null || typeof raw !== 'object') return null;
|
||
const d = raw as {
|
||
title?: string | null;
|
||
intro?: string | null;
|
||
items?: Array<{
|
||
title?: string | null;
|
||
listCaption?: string | null;
|
||
modalDescription?: string | null;
|
||
image?: {
|
||
alt?: string | null;
|
||
asset?: {
|
||
_id?: string;
|
||
url?: string | null;
|
||
metadata?: {dimensions?: {width?: number | null; height?: number | null}} | null;
|
||
} | null;
|
||
} | null;
|
||
} | null> | null;
|
||
seo?: {title?: string | null; description?: string | null} | null;
|
||
};
|
||
const title = d.title?.trim();
|
||
if (!title) return null;
|
||
const items: AbiirPressItemCms[] = [];
|
||
for (const row of d.items ?? []) {
|
||
const t = row?.title?.trim();
|
||
if (!t) continue;
|
||
const img = normPressImage(row?.image);
|
||
items.push({
|
||
title: t,
|
||
listCaption: row?.listCaption?.trim() || '',
|
||
modalDescription: row?.modalDescription?.trim() || '',
|
||
image: img,
|
||
});
|
||
}
|
||
if (!items.length) return null;
|
||
const {seoTitle, seoDescription} = normSeo(d.seo, {
|
||
t: MOCK_PRESS_CMS.seoTitle,
|
||
d: MOCK_PRESS_CMS.seoDescription,
|
||
});
|
||
return {
|
||
title,
|
||
intro: d.intro?.trim() || '',
|
||
items,
|
||
seoTitle,
|
||
seoDescription,
|
||
};
|
||
}
|
||
|
||
function strArray(
|
||
raw: Array<string | null | undefined> | null | undefined,
|
||
): string[] {
|
||
if (!raw?.length) return [];
|
||
return raw.map((p) => (typeof p === 'string' ? p.trim() : '')).filter(Boolean);
|
||
}
|
||
|
||
function normLegalSections(
|
||
raw: unknown,
|
||
fallback: AbiirLegalSectionCms[],
|
||
): AbiirLegalSectionCms[] {
|
||
if (!Array.isArray(raw) || !raw.length) return fallback;
|
||
const out: AbiirLegalSectionCms[] = [];
|
||
for (const row of raw) {
|
||
if (row == null || typeof row !== 'object') continue;
|
||
const r = row as {
|
||
title?: string | null;
|
||
paragraphs?: (string | null)[] | null;
|
||
listItems?: (string | null)[] | null;
|
||
trailingParagraphs?: (string | null)[] | null;
|
||
};
|
||
const title = r.title?.trim();
|
||
if (!title) continue;
|
||
const paragraphs = strArray(r.paragraphs);
|
||
const listItems = strArray(r.listItems);
|
||
const trailingParagraphs = strArray(r.trailingParagraphs);
|
||
if (!paragraphs.length && !listItems.length && !trailingParagraphs.length) {
|
||
continue;
|
||
}
|
||
out.push({
|
||
title,
|
||
paragraphs,
|
||
listItems: listItems.length ? listItems : undefined,
|
||
trailingParagraphs: trailingParagraphs.length
|
||
? trailingParagraphs
|
||
: undefined,
|
||
});
|
||
}
|
||
return out.length ? out : fallback;
|
||
}
|
||
|
||
export function normalizeLegalPagePayload(
|
||
raw: unknown,
|
||
defaults: AbiirLegalPageCms,
|
||
): AbiirLegalPageCms {
|
||
if (raw == null || typeof raw !== 'object') return defaults;
|
||
const d = raw as {
|
||
title?: string | null;
|
||
heroEyebrow?: string | null;
|
||
lastUpdatedLine?: string | null;
|
||
lead?: string | null;
|
||
sections?: unknown;
|
||
seo?: {title?: string | null; description?: string | null} | null;
|
||
};
|
||
const {seoTitle, seoDescription} = normSeo(d.seo, {
|
||
t: defaults.seoTitle,
|
||
d: defaults.seoDescription,
|
||
});
|
||
return {
|
||
title: d.title?.trim() || defaults.title,
|
||
heroEyebrow: d.heroEyebrow?.trim() || defaults.heroEyebrow,
|
||
lastUpdatedLine: d.lastUpdatedLine?.trim() || defaults.lastUpdatedLine,
|
||
lead: d.lead?.trim() || defaults.lead,
|
||
sections: normLegalSections(d.sections, defaults.sections),
|
||
seoTitle,
|
||
seoDescription,
|
||
};
|
||
}
|
||
|
||
// ─── Loaders ─────────────────────────────────────────────────────────────────
|
||
|
||
async function loadSingleton<T>(sanity: SanityContext, groq: string): Promise<T | null> {
|
||
const initial: QueryResponseInitial<T | null> = await sanity
|
||
.loadQuery<T>(groq)
|
||
.catch(
|
||
(): QueryResponseInitial<T | null> => ({data: null, sourceMap: undefined}),
|
||
);
|
||
return unwrapSanityQueryResult<T | null>(initial);
|
||
}
|
||
|
||
export async function loadFaqFromSanity(args: {sanity: SanityContext}) {
|
||
const data = await loadSingleton<unknown>(args.sanity, ABIIR_FAQ_PAGE_GROQ);
|
||
return {faqCms: normalizeFaqPayload(data) ?? MOCK_FAQ_CMS};
|
||
}
|
||
|
||
export async function loadShippingFromSanity(args: {sanity: SanityContext}) {
|
||
const data = await loadSingleton<unknown>(args.sanity, ABIIR_SHIPPING_PAGE_GROQ);
|
||
return {cms: normalizeShippingPagePayload(data)};
|
||
}
|
||
|
||
export async function loadTrackOrderFromSanity(args: {sanity: SanityContext}) {
|
||
const data = await loadSingleton<unknown>(args.sanity, ABIIR_TRACK_PAGE_GROQ);
|
||
return {cms: normalizeInfoPayload(data, MOCK_TRACK_ORDER_PAGE) ?? MOCK_TRACK_ORDER_PAGE};
|
||
}
|
||
|
||
export async function loadSkincareQuizFromSanity(args: {sanity: SanityContext}) {
|
||
const data = await loadSingleton<unknown>(args.sanity, ABIIR_QUIZ_PAGE_GROQ);
|
||
return {cms: normalizeInfoPayload(data, MOCK_SKINCARE_QUIZ_PAGE) ?? MOCK_SKINCARE_QUIZ_PAGE};
|
||
}
|
||
|
||
export async function loadContactFromSanity(args: {sanity: SanityContext}) {
|
||
const data = await loadSingleton<unknown>(args.sanity, ABIIR_CONTACT_PAGE_GROQ);
|
||
return {cms: normalizeContactPayload(data) ?? MOCK_CONTACT_CMS};
|
||
}
|
||
|
||
export async function loadPressFromSanity(args: {sanity: SanityContext}) {
|
||
const data = await loadSingleton<unknown>(args.sanity, ABIIR_PRESS_PAGE_GROQ);
|
||
return {pressCms: normalizePressPayload(data) ?? MOCK_PRESS_CMS};
|
||
}
|
||
|
||
export async function loadPrivacyPolicyFromSanity(args: {sanity: SanityContext}) {
|
||
const data = await loadSingleton<unknown>(args.sanity, ABIIR_PRIVACY_POLICY_PAGE_GROQ);
|
||
return {cms: normalizeLegalPagePayload(data, MOCK_PRIVACY_POLICY_PAGE)};
|
||
}
|
||
|
||
export async function loadTermsOfServiceFromSanity(args: {sanity: SanityContext}) {
|
||
const data = await loadSingleton<unknown>(args.sanity, ABIIR_TERMS_OF_SERVICE_PAGE_GROQ);
|
||
return {cms: normalizeLegalPagePayload(data, MOCK_TERMS_OF_SERVICE_PAGE)};
|
||
}
|