440 lines
14 KiB
TypeScript
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 />
|
|
</>
|
|
);
|
|
}
|