trptk/app/page.tsx
2026-02-24 17:14:07 +01:00

440 lines
14 KiB
TypeScript

import type { Metadata } from "next";
import { cache } from "react";
import Link from "next/link";
import { Header } from "@/components/header/Header";
import { Footer } from "@/components/footer/Footer";
import { defineQuery } from "next-sanity";
import { sanity } from "@/lib/sanity";
import { ReleaseCover } from "@/components/release/ReleaseCover";
import { urlFor } from "@/lib/sanityImage";
import { ARTIST_PLACEHOLDER_SRC, CARD_GRID_CLASSES_4 } from "@/lib/constants";
import { ArrowLink } from "@/components/ArrowLink";
import { IconButtonLink } from "@/components/IconButton";
import { IoPersonOutline } from "react-icons/io5";
import { ReleaseCard, type ReleaseCardData } from "@/components/release/ReleaseCard";
import { BlogCard, type BlogCardData } from "@/components/blog/BlogCard";
import { ConcertTable, type ConcertData } from "@/components/concert/ConcertTable";
import { formatYears } from "@/components/artist/types";
import type { SanityImageSource } from "@sanity/image-url";
import type { TrackData } from "@/components/release/Tracklist";
import { FeaturedReleaseActions } from "@/components/release/FeaturedReleaseActions";
export const revalidate = 86400;
type FeaturedAlbum = {
name?: string;
albumArtist?: string;
slug?: string;
albumCover?: SanityImageSource;
shortDescription?: string;
tracks?: TrackData[];
};
type FeaturedArtist = {
name?: string;
role?: string;
slug?: string;
image?: SanityImageSource;
bioExcerpt?: string;
};
type FeaturedComposer = {
name?: string;
slug?: string;
birthYear?: number;
deathYear?: number;
image?: SanityImageSource;
bioExcerpt?: string;
};
type HomeSettings = {
featuredAlbum: FeaturedAlbum | null;
featuredArtist: FeaturedArtist | null;
featuredComposer: FeaturedComposer | null;
};
const SETTINGS_QUERY = defineQuery(`
*[_type == "settings"][0]{
"featuredAlbum": featuredAlbum->{
name,
albumArtist,
"slug": slug.current,
albumCover,
shortDescription,
"tracks": tracks[]{
"workId": work->_id,
"workTitle": work->title,
"composerName": work->composer->name,
"arrangerName": work->arranger->name,
movement,
displayTitle,
duration,
artist,
"previewMp3Url": previewMp3.asset->url
}
},
"featuredArtist": featuredArtist->{
name,
role,
"slug": slug.current,
image,
"bioExcerpt": pt::text(bio)
},
"featuredComposer": featuredComposer->{
name,
"slug": slug.current,
birthYear,
deathYear,
image,
"bioExcerpt": pt::text(bio)
}
}
`);
const LATEST_RELEASES_QUERY = defineQuery(`
*[_type == "release"] | order(coalesce(releaseDate, "0000-01-01") desc) [0...8] {
_id,
name,
albumArtist,
catalogNo,
releaseDate,
"slug": slug.current,
albumCover
}
`);
const LATEST_BLOGS_QUERY = defineQuery(`
*[_type == "blog"] | order(coalesce(publishDate, "0000-01-01") desc) [0...8] {
_id,
title,
subtitle,
author,
publishDate,
category,
"slug": slug.current,
featuredImage
}
`);
const HOME_UPCOMING_CONCERTS_QUERY = defineQuery(`
*[_type == "concert" && date >= $today]
{
_id,
title,
subtitle,
date,
time,
locationName,
city,
country,
"artists": artists[]->{ _id, name, "slug": slug.current },
ticketUrl
}
| order(date asc, time asc) [0...10]
`);
const getHomeSettings = cache(async () => {
try {
return await sanity.fetch<HomeSettings>(SETTINGS_QUERY);
} catch (error) {
console.error("Failed to fetch home settings:", error);
return { featuredAlbum: null, featuredArtist: null, featuredComposer: null };
}
});
export async function generateMetadata(): Promise<Metadata> {
const { featuredAlbum: release } = await getHomeSettings();
const description = release?.shortDescription ?? "Recording the extraordinary.";
const ogImage = release?.albumCover
? urlFor(release.albumCover).width(1200).height(1200).url()
: undefined;
return {
title: "TRPTK • Recording the extraordinary",
description,
alternates: { canonical: "/" },
openGraph: {
title: "TRPTK",
description,
type: "website",
...(ogImage && {
images: [
{ url: ogImage, width: 1200, height: 1200, alt: "TRPTK • Recording the extraordinary" },
],
}),
},
twitter: {
card: "summary_large_image",
title: "TRPTK",
description,
...(ogImage && { images: [ogImage] }),
},
};
}
function cardVisibility(index: number) {
if (index < 4) return "";
if (index < 6) return "hidden md:block";
return "hidden lg:block";
}
export default async function Home() {
const today = new Date().toISOString().slice(0, 10);
const [
{ featuredAlbum: release, featuredArtist: artist, featuredComposer: composer },
latestReleases,
latestBlogs,
upcomingConcerts,
] = await Promise.all([
getHomeSettings(),
sanity.fetch<ReleaseCardData[]>(LATEST_RELEASES_QUERY),
sanity.fetch<BlogCardData[]>(LATEST_BLOGS_QUERY),
sanity.fetch<ConcertData[]>(HOME_UPCOMING_CONCERTS_QUERY, { today }),
]);
const displayName = release?.name ?? "";
const displayArtist = release?.albumArtist ?? "";
const jsonLd = {
"@context": "https://schema.org",
"@type": "WebPage",
name: "TRPTK",
url: "https://trptk.com",
description: "Recording the extraordinary.",
...(release && {
mainEntity: {
"@type": "MusicAlbum",
name: release.name,
...(release.slug && { url: `https://trptk.com/release/${release.slug}` }),
...(release.albumArtist && {
byArtist: { "@type": "MusicGroup", name: release.albumArtist },
}),
...(release.albumCover && {
image: urlFor(release.albumCover).width(800).url(),
}),
},
}),
};
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd).replace(/</g, "\\u003c") }}
/>
<Header />
<section className="mx-auto my-auto max-w-250 px-6 py-12 font-silka md:px-8 md:py-16">
{/* Featued Album */}
{release && (
<div className="mx-auto grid grid-cols-1 gap-10 sm:grid-cols-2 sm:gap-8 md:gap-12 lg:gap-20">
<div className="order-2 content-center sm:order-1">
<h3 className="mb-2 font-silka text-sm break-words text-lightsec dark:text-darksec">
Featured album
</h3>
{release.slug ? (
<Link href={`/release/${release.slug}`}>
<h1 className="mb-2 font-argesta text-3xl break-words">{displayName}</h1>
</Link>
) : (
<h1 className="mb-2 font-argesta text-3xl break-words">{displayName}</h1>
)}
<h2 className="mb-6 font-silka text-base break-words text-lightsec dark:text-darksec">
{displayArtist}
</h2>
{release.shortDescription && (
<p className="line-clamp-2 text-sm md:line-clamp-3 lg:line-clamp-4">
{release.shortDescription}
</p>
)}
{release.slug && (
<FeaturedReleaseActions
tracks={release.tracks ?? []}
albumCover={release.albumCover}
albumArtist={release.albumArtist}
releaseSlug={release.slug}
/>
)}
</div>
<div className="order-1 content-center sm:order-2">
{release.slug ? (
<Link href={`/release/${release.slug}`}>
<ReleaseCover
src={
release.albumCover ? urlFor(release.albumCover).url() : ARTIST_PLACEHOLDER_SRC
}
alt={`Album cover for ${displayName} by ${displayArtist}`}
/>
</Link>
) : (
<ReleaseCover
src={
release.albumCover ? urlFor(release.albumCover).url() : ARTIST_PLACEHOLDER_SRC
}
alt={`Album cover for ${displayName} by ${displayArtist}`}
/>
)}
</div>
</div>
)}
</section>
{/* Latest releases */}
{latestReleases.length > 0 && (
<section className="mx-auto max-w-300 px-6 py-12 font-silka md:px-8 md:py-16 2xl:max-w-400">
<h2 className="font-argesta text-3xl">Latest releases</h2>
<ArrowLink
href="/releases"
className="mt-2 mb-8 inline-block text-sm text-lightsec dark:text-darksec"
>
Browse all releases
</ArrowLink>
<div className={CARD_GRID_CLASSES_4}>
{latestReleases.map((r, i) => (
<ReleaseCard key={r._id} release={r} className={cardVisibility(i)} />
))}
</div>
</section>
)}
{/* Featured Artist */}
{artist && (
<section className="mx-auto my-auto max-w-250 px-6 py-12 font-silka md:px-8 md:py-16">
<div className="mx-auto grid grid-cols-1 gap-10 sm:grid-cols-2 sm:gap-8 md:gap-12 lg:gap-20">
<div className="content-center">
{artist.slug ? (
<Link href={`/artist/${artist.slug}`}>
<ReleaseCover
src={artist.image ? urlFor(artist.image).url() : ARTIST_PLACEHOLDER_SRC}
alt={`Photo of ${artist.name}`}
/>
</Link>
) : (
<ReleaseCover
src={artist.image ? urlFor(artist.image).url() : ARTIST_PLACEHOLDER_SRC}
alt={`Photo of ${artist.name}`}
/>
)}
</div>
<div className="content-center">
<h3 className="mb-2 font-silka text-sm break-words text-lightsec dark:text-darksec">
Featured artist
</h3>
{artist.slug ? (
<Link href={`/artist/${artist.slug}`}>
<h2 className="mb-2 font-argesta text-3xl break-words">{artist.name}</h2>
</Link>
) : (
<h2 className="mb-2 font-argesta text-3xl break-words">{artist.name}</h2>
)}
<h3 className="mb-6 font-silka text-base break-words text-lightsec dark:text-darksec">
{artist.role}
</h3>
{artist.bioExcerpt && (
<p className="line-clamp-2 text-sm md:line-clamp-3 lg:line-clamp-4">
{artist.bioExcerpt}
</p>
)}
{artist.slug && (
<div className="mt-4 flex gap-3">
<IconButtonLink href={`/artist/${artist.slug}`} aria-label="View artist">
<IoPersonOutline />
</IconButtonLink>
</div>
)}
</div>
</div>
</section>
)}
{/* From the blog */}
{latestBlogs.length > 0 && (
<section className="mx-auto max-w-300 px-6 py-12 font-silka md:px-8 md:py-16 2xl:max-w-400">
<h2 className="font-argesta text-3xl">From the blog</h2>
<ArrowLink
href="/blog"
className="mt-2 mb-8 inline-block text-sm text-lightsec dark:text-darksec"
>
Browse all posts
</ArrowLink>
<div className={CARD_GRID_CLASSES_4}>
{latestBlogs.map((b, i) => (
<BlogCard key={b._id} blog={b} className={cardVisibility(i)} />
))}
</div>
</section>
)}
{/* Featured Composer */}
{composer && (
<section className="mx-auto my-auto max-w-250 px-6 py-12 font-silka md:px-8 md:py-16">
<div className="mx-auto grid grid-cols-1 gap-10 sm:grid-cols-2 sm:gap-8 md:gap-12 lg:gap-20">
<div className="order-2 content-center sm:order-1">
<h3 className="mb-2 font-silka text-sm break-words text-lightsec dark:text-darksec">
Featured composer
</h3>
{composer.slug ? (
<Link href={`/composer/${composer.slug}`}>
<h2 className="mb-2 font-argesta text-3xl break-words">{composer.name}</h2>
</Link>
) : (
<h2 className="mb-2 font-argesta text-3xl break-words">{composer.name}</h2>
)}
<h3 className="mb-6 font-silka text-base break-words text-lightsec dark:text-darksec">
{formatYears(composer.birthYear, composer.deathYear)}
</h3>
{composer.bioExcerpt && (
<p className="line-clamp-2 text-sm md:line-clamp-3 lg:line-clamp-4">
{composer.bioExcerpt}
</p>
)}
{composer.slug && (
<div className="mt-4 flex gap-3">
<IconButtonLink href={`/composer/${composer.slug}`} aria-label="View composer">
<IoPersonOutline />
</IconButtonLink>
</div>
)}
</div>
<div className="order-1 content-center sm:order-2">
{composer.slug ? (
<Link href={`/composer/${composer.slug}`}>
<ReleaseCover
src={composer.image ? urlFor(composer.image).url() : ARTIST_PLACEHOLDER_SRC}
alt={`Portrait of ${composer.name}`}
/>
</Link>
) : (
<ReleaseCover
src={composer.image ? urlFor(composer.image).url() : ARTIST_PLACEHOLDER_SRC}
alt={`Portrait of ${composer.name}`}
/>
)}
</div>
</div>
</section>
)}
{/* Upcoming concerts */}
{upcomingConcerts.length > 0 && (
<section className="mx-auto max-w-300 px-6 py-12 font-silka md:px-8 md:py-16 2xl:max-w-400">
<h2 className="font-argesta text-3xl">Upcoming concerts</h2>
<ArrowLink
href="/concerts"
className="mt-2 mb-8 inline-block text-sm text-lightsec dark:text-darksec"
>
Browse all concerts
</ArrowLink>
<ConcertTable concerts={upcomingConcerts} />
</section>
)}
<Footer />
</>
);
}