Adds homePage singleton document type (hero headline/mission, video section, CTA cards, sponsors). Home page fetches from Sanity when configured and falls back to the existing hardcoded values when not. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
141 lines
4.6 KiB
TypeScript
141 lines
4.6 KiB
TypeScript
import { createClient } from '@sanity/client';
|
|
import { toHTML } from '@portabletext/to-html';
|
|
import type { NewsArticle } from './news';
|
|
|
|
// ── Client ────────────────────────────────────────────────────────────────────
|
|
|
|
export const sanityClient = createClient({
|
|
projectId: import.meta.env.SANITY_PROJECT_ID ?? '',
|
|
dataset: import.meta.env.SANITY_DATASET ?? 'production',
|
|
apiVersion: '2024-01-01',
|
|
// useCdn in production; always false during dev so edits show immediately
|
|
useCdn: import.meta.env.PROD ?? false,
|
|
// Token only needed for draft previews; public published content works without one
|
|
token: import.meta.env.SANITY_API_TOKEN,
|
|
});
|
|
|
|
export function isSanityConfigured(): boolean {
|
|
return Boolean(import.meta.env.SANITY_PROJECT_ID);
|
|
}
|
|
|
|
// ── GROQ queries ──────────────────────────────────────────────────────────────
|
|
|
|
const POST_FIELDS = /* groq */ `
|
|
_id,
|
|
title,
|
|
"slug": slug.current,
|
|
date,
|
|
category,
|
|
summary,
|
|
"image": image.asset->url,
|
|
body
|
|
`;
|
|
|
|
const ALL_POSTS_QUERY = /* groq */ `*[_type == "post"] | order(date desc) { ${POST_FIELDS} }`;
|
|
const POST_BY_SLUG_QUERY = /* groq */ `*[_type == "post" && slug.current == $slug][0] { ${POST_FIELDS} }`;
|
|
|
|
// ── Raw Sanity shape ──────────────────────────────────────────────────────────
|
|
|
|
interface SanityPost {
|
|
_id: string;
|
|
title: string;
|
|
slug: string;
|
|
date: string;
|
|
category: string;
|
|
summary: string;
|
|
image?: string | null;
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
body?: any[];
|
|
}
|
|
|
|
// ── Converters ────────────────────────────────────────────────────────────────
|
|
|
|
function toNewsArticle(post: SanityPost): NewsArticle {
|
|
const html = post.body
|
|
? toHTML(post.body, {
|
|
components: {
|
|
marks: {
|
|
link: ({ children, value }) => {
|
|
const target = value?.blank ? ' target="_blank" rel="noreferrer"' : '';
|
|
return `<a href="${value?.href ?? '#'}"${target}>${children}</a>`;
|
|
},
|
|
},
|
|
},
|
|
})
|
|
: '';
|
|
|
|
return {
|
|
title: post.title,
|
|
date: post.date ?? '',
|
|
summary: post.summary ?? '',
|
|
image: post.image ?? undefined,
|
|
slug: post.slug,
|
|
category: post.category ?? 'News',
|
|
body: '', // raw markdown not used when coming from Sanity
|
|
html,
|
|
};
|
|
}
|
|
|
|
// ── Home page ─────────────────────────────────────────────────────────────────
|
|
|
|
export interface SanityCtaCard {
|
|
label: string;
|
|
title: string;
|
|
description: string;
|
|
image?: string | null;
|
|
href: string;
|
|
}
|
|
|
|
export interface SanitySponsor {
|
|
name: string;
|
|
logo?: string | null;
|
|
url?: string | null;
|
|
}
|
|
|
|
export interface HomePageData {
|
|
heroHeadline: string;
|
|
heroMission: string;
|
|
videoHeading: string;
|
|
videoBody: string;
|
|
videoYoutubeId: string;
|
|
ctaCards: SanityCtaCard[];
|
|
sponsorHeading: string;
|
|
sponsors: SanitySponsor[];
|
|
}
|
|
|
|
const HOME_PAGE_QUERY = /* groq */ `*[_type == "homePage"][0]{
|
|
heroHeadline,
|
|
heroMission,
|
|
videoHeading,
|
|
videoBody,
|
|
videoYoutubeId,
|
|
ctaCards[]{
|
|
label,
|
|
title,
|
|
description,
|
|
"image": image.asset->url,
|
|
href,
|
|
},
|
|
sponsorHeading,
|
|
sponsors[]{
|
|
name,
|
|
"logo": logo.asset->url,
|
|
url,
|
|
}
|
|
}`;
|
|
|
|
export async function getSanityHomePage(): Promise<HomePageData | null> {
|
|
return sanityClient.fetch<HomePageData | null>(HOME_PAGE_QUERY);
|
|
}
|
|
|
|
// ── Public API ────────────────────────────────────────────────────────────────
|
|
|
|
export async function getSanityPosts(): Promise<NewsArticle[]> {
|
|
const posts = await sanityClient.fetch<SanityPost[]>(ALL_POSTS_QUERY);
|
|
return posts.map(toNewsArticle);
|
|
}
|
|
|
|
export async function getSanityPost(slug: string): Promise<NewsArticle | undefined> {
|
|
const post = await sanityClient.fetch<SanityPost | null>(POST_BY_SLUG_QUERY, { slug });
|
|
return post ? toNewsArticle(post) : undefined;
|
|
}
|