From 2409fecfef9c9e7168881d51fb56e33c7da0493a Mon Sep 17 00:00:00 2001 From: Brendon Heinst Date: Tue, 24 Feb 2026 17:14:07 +0100 Subject: [PATCH] Initial commit --- .dockerignore | 4 + .gitignore | 42 + .prettierrc | 5 + .vscode/settings.json | 2 + Dockerfile | 32 + app/[slug]/layout.tsx | 12 + app/[slug]/page.tsx | 135 + app/account/layout.tsx | 18 + app/account/login/page.tsx | 32 + app/account/page.tsx | 48 + app/android-chrome-192x192.png | Bin 0 -> 5108 bytes app/android-chrome-512x512.png | Bin 0 -> 12871 bytes app/api/account/addresses/[id]/route.ts | 75 + app/api/account/addresses/route.ts | 60 + app/api/account/downloads/route.ts | 221 + app/api/account/login/route.ts | 99 + app/api/account/logout/route.ts | 12 + app/api/account/me/route.ts | 20 + app/api/account/orders/route.ts | 22 + app/api/account/register/route.ts | 176 + app/api/cart/[cartId]/items/[itemId]/route.ts | 55 + app/api/cart/[cartId]/items/route.ts | 42 + app/api/cart/[cartId]/promotions/route.ts | 70 + app/api/cart/route.ts | 58 + app/api/checkout/complete/route.ts | 32 + app/api/checkout/downloads/route.ts | 47 + app/api/checkout/payment/route.ts | 46 + app/api/checkout/shipping-method/route.ts | 29 + app/api/checkout/shipping-options/route.ts | 22 + app/api/checkout/update/route.ts | 53 + app/api/search/route.ts | 109 + app/apple-touch-icon.png | Bin 0 -> 4651 bytes app/artist/[slug]/layout.tsx | 12 + app/artist/[slug]/page.tsx | 203 + app/artists/error.tsx | 19 + app/artists/layout.tsx | 12 + app/artists/page.tsx | 153 + app/blog/(archive)/error.tsx | 19 + app/blog/(archive)/layout.tsx | 12 + app/blog/(archive)/page.tsx | 175 + app/blog/[slug]/layout.tsx | 12 + app/blog/[slug]/page.tsx | 305 + app/checkout/layout.tsx | 18 + app/checkout/page.tsx | 806 + app/checkout/return/page.tsx | 248 + app/composer/[slug]/layout.tsx | 12 + app/composer/[slug]/page.tsx | 214 + app/composers/error.tsx | 19 + app/composers/layout.tsx | 12 + app/composers/page.tsx | 180 + app/concerts/error.tsx | 19 + app/concerts/layout.tsx | 12 + app/concerts/page.tsx | 115 + app/favicon-16x16.png | Bin 0 -> 395 bytes app/favicon-32x32.png | Bin 0 -> 721 bytes app/favicon.ico | Bin 0 -> 15406 bytes app/globals.css | 106 + app/layout.tsx | 102 + app/page.tsx | 440 + app/release/[slug]/booklet/route.ts | 50 + app/release/[slug]/layout.tsx | 11 + app/release/[slug]/page.tsx | 502 + app/releases/error.tsx | 19 + app/releases/layout.tsx | 12 + app/releases/page.tsx | 259 + app/robots.ts | 25 + app/site.webmanifest | 1 + app/sitemap.ts | 68 + app/work/[slug]/layout.tsx | 12 + app/work/[slug]/page.tsx | 217 + components/AnimatedText.tsx | 43 + components/ArrowLink.tsx | 46 + components/Breadcrumb.tsx | 43 + components/CountrySelect.tsx | 116 + components/FilterDropdown.tsx | 142 + components/IconButton.tsx | 47 + components/IconButtonMini.tsx | 46 + components/PaginationNav.tsx | 72 + components/SearchBar.tsx | 178 + components/SortDropdown.tsx | 130 + components/TabsClient.tsx | 103 + components/account/AccountTabs.tsx | 28 + components/account/AddressesTab.tsx | 277 + components/account/DownloadCard.tsx | 142 + components/account/DownloadsTab.tsx | 379 + components/account/OrdersTab.tsx | 274 + components/account/ProfileTab.tsx | 14 + components/artist/ArtistCard.tsx | 76 + components/artist/ArtistCardCompact.tsx | 51 + components/artist/ArtistConcertsTab.tsx | 25 + components/artist/ArtistReleasesTab.tsx | 265 + components/artist/ArtistTabs.tsx | 54 + components/artist/ArtistWorksTab.tsx | 95 + components/artist/types.ts | 25 + components/auth/AccountButton.tsx | 19 + components/auth/AuthContext.tsx | 132 + components/auth/AuthForm.tsx | 291 + components/blog/BlogCard.tsx | 69 + components/cart/CartButton.tsx | 60 + components/cart/CartContext.tsx | 195 + components/cart/CartDrawer.tsx | 255 + components/concert/ConcertTable.tsx | 122 + components/footer/Footer.tsx | 76 + components/header/Header.tsx | 219 + components/header/MenuToggleButton.tsx | 17 + components/header/SocialButtons.tsx | 39 + components/header/ThemeToggleButton.tsx | 25 + components/icons/QobuzIcon.tsx | 54 + components/icons/TrptkLogo.tsx | 25 + components/player/Player.tsx | 139 + components/player/PlayerContext.tsx | 208 + components/release/BookletLink.tsx | 26 + components/release/DetailTable.tsx | 40 + components/release/FeaturedReleaseActions.tsx | 39 + components/release/FormatSelector.tsx | 314 + components/release/ReleaseCard.tsx | 73 + components/release/ReleaseCover.tsx | 59 + components/release/StreamingLinks.tsx | 51 + components/release/Tracklist.tsx | 432 + components/search/GlobalSearch.tsx | 337 + eslint.config.mjs | 18 + hooks/useClickOutside.ts | 24 + hooks/useDebounced.ts | 12 + lib/apiUtils.ts | 122 + lib/auth.ts | 42 + lib/cartUtils.ts | 32 + lib/constants.ts | 3 + lib/countries.ts | 197 + lib/diacritics.ts | 4 + lib/duration.ts | 22 + lib/listHelpers.ts | 16 + lib/medusa.ts | 374 + lib/portableTextComponents.tsx | 53 + lib/rateLimit.ts | 76 + lib/release.ts | 65 + lib/sanity.ts | 18 + lib/sanityImage.ts | 9 + lib/sanityImageLoader.ts | 23 + lib/variants.ts | 186 + next.config.ts | 65 + package-lock.json | 18642 ++++++++++++++++ package.json | 38 + postcss.config.mjs | 7 + public/fonts/Silka-Regular.woff | Bin 0 -> 22248 bytes public/fonts/Silka-Regular.woff2 | Bin 0 -> 16076 bytes public/fonts/Silka-SemiBold.woff | Bin 0 -> 23588 bytes public/fonts/Silka-SemiBold.woff2 | Bin 0 -> 17096 bytes public/fonts/argesta.woff | Bin 0 -> 27532 bytes public/fonts/argesta.woff2 | Bin 0 -> 20892 bytes public/images/placeholder-artist.png | Bin 0 -> 13550 bytes sanity-typegen.json | 8 + sanity.types.ts | 2500 +++ tailwind.config.js | 16 + tsconfig.json | 34 + 154 files changed, 34572 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 .prettierrc create mode 100644 .vscode/settings.json create mode 100644 Dockerfile create mode 100644 app/[slug]/layout.tsx create mode 100644 app/[slug]/page.tsx create mode 100644 app/account/layout.tsx create mode 100644 app/account/login/page.tsx create mode 100644 app/account/page.tsx create mode 100755 app/android-chrome-192x192.png create mode 100755 app/android-chrome-512x512.png create mode 100644 app/api/account/addresses/[id]/route.ts create mode 100644 app/api/account/addresses/route.ts create mode 100644 app/api/account/downloads/route.ts create mode 100644 app/api/account/login/route.ts create mode 100644 app/api/account/logout/route.ts create mode 100644 app/api/account/me/route.ts create mode 100644 app/api/account/orders/route.ts create mode 100644 app/api/account/register/route.ts create mode 100644 app/api/cart/[cartId]/items/[itemId]/route.ts create mode 100644 app/api/cart/[cartId]/items/route.ts create mode 100644 app/api/cart/[cartId]/promotions/route.ts create mode 100644 app/api/cart/route.ts create mode 100644 app/api/checkout/complete/route.ts create mode 100644 app/api/checkout/downloads/route.ts create mode 100644 app/api/checkout/payment/route.ts create mode 100644 app/api/checkout/shipping-method/route.ts create mode 100644 app/api/checkout/shipping-options/route.ts create mode 100644 app/api/checkout/update/route.ts create mode 100644 app/api/search/route.ts create mode 100755 app/apple-touch-icon.png create mode 100644 app/artist/[slug]/layout.tsx create mode 100644 app/artist/[slug]/page.tsx create mode 100644 app/artists/error.tsx create mode 100644 app/artists/layout.tsx create mode 100644 app/artists/page.tsx create mode 100644 app/blog/(archive)/error.tsx create mode 100644 app/blog/(archive)/layout.tsx create mode 100644 app/blog/(archive)/page.tsx create mode 100644 app/blog/[slug]/layout.tsx create mode 100644 app/blog/[slug]/page.tsx create mode 100644 app/checkout/layout.tsx create mode 100644 app/checkout/page.tsx create mode 100644 app/checkout/return/page.tsx create mode 100644 app/composer/[slug]/layout.tsx create mode 100644 app/composer/[slug]/page.tsx create mode 100644 app/composers/error.tsx create mode 100644 app/composers/layout.tsx create mode 100644 app/composers/page.tsx create mode 100644 app/concerts/error.tsx create mode 100644 app/concerts/layout.tsx create mode 100644 app/concerts/page.tsx create mode 100755 app/favicon-16x16.png create mode 100755 app/favicon-32x32.png create mode 100755 app/favicon.ico create mode 100644 app/globals.css create mode 100644 app/layout.tsx create mode 100644 app/page.tsx create mode 100644 app/release/[slug]/booklet/route.ts create mode 100644 app/release/[slug]/layout.tsx create mode 100644 app/release/[slug]/page.tsx create mode 100644 app/releases/error.tsx create mode 100644 app/releases/layout.tsx create mode 100644 app/releases/page.tsx create mode 100644 app/robots.ts create mode 100755 app/site.webmanifest create mode 100644 app/sitemap.ts create mode 100644 app/work/[slug]/layout.tsx create mode 100644 app/work/[slug]/page.tsx create mode 100644 components/AnimatedText.tsx create mode 100644 components/ArrowLink.tsx create mode 100644 components/Breadcrumb.tsx create mode 100644 components/CountrySelect.tsx create mode 100644 components/FilterDropdown.tsx create mode 100644 components/IconButton.tsx create mode 100644 components/IconButtonMini.tsx create mode 100644 components/PaginationNav.tsx create mode 100644 components/SearchBar.tsx create mode 100644 components/SortDropdown.tsx create mode 100644 components/TabsClient.tsx create mode 100644 components/account/AccountTabs.tsx create mode 100644 components/account/AddressesTab.tsx create mode 100644 components/account/DownloadCard.tsx create mode 100644 components/account/DownloadsTab.tsx create mode 100644 components/account/OrdersTab.tsx create mode 100644 components/account/ProfileTab.tsx create mode 100644 components/artist/ArtistCard.tsx create mode 100644 components/artist/ArtistCardCompact.tsx create mode 100644 components/artist/ArtistConcertsTab.tsx create mode 100644 components/artist/ArtistReleasesTab.tsx create mode 100644 components/artist/ArtistTabs.tsx create mode 100644 components/artist/ArtistWorksTab.tsx create mode 100644 components/artist/types.ts create mode 100644 components/auth/AccountButton.tsx create mode 100644 components/auth/AuthContext.tsx create mode 100644 components/auth/AuthForm.tsx create mode 100644 components/blog/BlogCard.tsx create mode 100644 components/cart/CartButton.tsx create mode 100644 components/cart/CartContext.tsx create mode 100644 components/cart/CartDrawer.tsx create mode 100644 components/concert/ConcertTable.tsx create mode 100644 components/footer/Footer.tsx create mode 100644 components/header/Header.tsx create mode 100644 components/header/MenuToggleButton.tsx create mode 100644 components/header/SocialButtons.tsx create mode 100644 components/header/ThemeToggleButton.tsx create mode 100644 components/icons/QobuzIcon.tsx create mode 100644 components/icons/TrptkLogo.tsx create mode 100644 components/player/Player.tsx create mode 100644 components/player/PlayerContext.tsx create mode 100644 components/release/BookletLink.tsx create mode 100644 components/release/DetailTable.tsx create mode 100644 components/release/FeaturedReleaseActions.tsx create mode 100644 components/release/FormatSelector.tsx create mode 100644 components/release/ReleaseCard.tsx create mode 100644 components/release/ReleaseCover.tsx create mode 100644 components/release/StreamingLinks.tsx create mode 100644 components/release/Tracklist.tsx create mode 100644 components/search/GlobalSearch.tsx create mode 100644 eslint.config.mjs create mode 100644 hooks/useClickOutside.ts create mode 100644 hooks/useDebounced.ts create mode 100644 lib/apiUtils.ts create mode 100644 lib/auth.ts create mode 100644 lib/cartUtils.ts create mode 100644 lib/constants.ts create mode 100644 lib/countries.ts create mode 100644 lib/diacritics.ts create mode 100644 lib/duration.ts create mode 100644 lib/listHelpers.ts create mode 100644 lib/medusa.ts create mode 100644 lib/portableTextComponents.tsx create mode 100644 lib/rateLimit.ts create mode 100644 lib/release.ts create mode 100644 lib/sanity.ts create mode 100644 lib/sanityImage.ts create mode 100644 lib/sanityImageLoader.ts create mode 100644 lib/variants.ts create mode 100644 next.config.ts create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 postcss.config.mjs create mode 100644 public/fonts/Silka-Regular.woff create mode 100644 public/fonts/Silka-Regular.woff2 create mode 100644 public/fonts/Silka-SemiBold.woff create mode 100644 public/fonts/Silka-SemiBold.woff2 create mode 100644 public/fonts/argesta.woff create mode 100644 public/fonts/argesta.woff2 create mode 100644 public/images/placeholder-artist.png create mode 100644 sanity-typegen.json create mode 100644 sanity.types.ts create mode 100644 tailwind.config.js create mode 100644 tsconfig.json diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..79c56cd --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +node_modules +.next +.env* +.git diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5c0c43b --- /dev/null +++ b/.gitignore @@ -0,0 +1,42 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +/.vscode/ +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..587193f --- /dev/null +++ b/.prettierrc @@ -0,0 +1,5 @@ +{ + "printWidth": 100, + "plugins": ["prettier-plugin-tailwindcss"], + "tailwindStylesheet": "./app/globals.css" +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..7a73a41 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..54a6600 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,32 @@ +FROM node:20-alpine AS builder +WORKDIR /app + +# Only NEXT_PUBLIC_ vars are needed at build time (inlined into client bundle) +ARG NEXT_PUBLIC_APP_URL +ARG NEXT_PUBLIC_SANITY_PROJECT_ID +ARG NEXT_PUBLIC_SANITY_DATASET +ARG NEXT_PUBLIC_SANITY_API_VERSION + +ENV NEXT_PUBLIC_APP_URL=$NEXT_PUBLIC_APP_URL +ENV NEXT_PUBLIC_SANITY_PROJECT_ID=$NEXT_PUBLIC_SANITY_PROJECT_ID +ENV NEXT_PUBLIC_SANITY_DATASET=$NEXT_PUBLIC_SANITY_DATASET +ENV NEXT_PUBLIC_SANITY_API_VERSION=$NEXT_PUBLIC_SANITY_API_VERSION + +COPY package.json package-lock.json* ./ +RUN npm ci +COPY . . +RUN npm run build + +FROM node:20-alpine +WORKDIR /app + +RUN addgroup -S nextjs && adduser -S nextjs -G nextjs + +COPY --from=builder --chown=nextjs:nextjs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nextjs /app/.next/static ./.next/static +COPY --from=builder --chown=nextjs:nextjs /app/public ./public + +USER nextjs +ENV NODE_ENV=production +EXPOSE 3000 +CMD ["node", "server.js"] diff --git a/app/[slug]/layout.tsx b/app/[slug]/layout.tsx new file mode 100644 index 0000000..b966d03 --- /dev/null +++ b/app/[slug]/layout.tsx @@ -0,0 +1,12 @@ +import { ThemeToggleButton } from "@/components/header/ThemeToggleButton"; + +export default function Layout({ children }: { children: React.ReactNode }) { + return ( + <> +
+ +
+ {children} + + ); +} diff --git a/app/[slug]/page.tsx b/app/[slug]/page.tsx new file mode 100644 index 0000000..0e2a116 --- /dev/null +++ b/app/[slug]/page.tsx @@ -0,0 +1,135 @@ +import { Metadata } from "next"; +import { notFound, redirect } from "next/navigation"; +import { cache } from "react"; +import { defineQuery } from "next-sanity"; +import { sanity } from "@/lib/sanity"; + +import { AnimatedText } from "@/components/AnimatedText"; +import { ReleaseCover } from "@/components/release/ReleaseCover"; +import { StreamingLinks } from "@/components/release/StreamingLinks"; +import { urlFor } from "@/lib/sanityImage"; +import { ARTIST_PLACEHOLDER_SRC } from "@/lib/constants"; +import { buildLinks, type Release } from "@/lib/release"; + +export const dynamicParams = true; +export const revalidate = 86400; + +const RELEASE_BY_CATALOG_NO_QUERY = defineQuery(` +*[_type == "release" && lower(catalogNo) == $slug][0]{ + name, + albumArtist, + label, + catalogNo, + albumCover, + officialUrl, + spotifyUrl, + appleMusicUrl, + deezerUrl, + amazonMusicUrl, + tidalUrl, + qobuzUrl, +} +`); + +const getRelease = cache(async (slug: string) => { + try { + const release = await sanity.fetch(RELEASE_BY_CATALOG_NO_QUERY, { slug }); + return release; + } catch (error) { + console.error("Failed to fetch release:", error); + return null; + } +}); + +const RELEASE_CATALOG_SLUGS_QUERY = defineQuery( + `*[_type == "release" && defined(catalogNo)][]{ "slug": lower(catalogNo) }`, +); + +export async function generateStaticParams() { + const releases = await sanity.fetch<{ slug: string }[]>(RELEASE_CATALOG_SLUGS_QUERY); + + return releases.map(({ slug }) => ({ slug })); +} + +export async function generateMetadata({ + params, +}: { + params: Promise<{ slug: string }>; +}): Promise { + const { slug } = await params; + + if (!slug) return { title: "TRPTK" }; + + const release = await getRelease(slug.toLowerCase()); + + if (!release) return { title: "TRPTK" }; + + const description = `Listen to ${release.name}${release.albumArtist ? ` by ${release.albumArtist}` : ""} on all major streaming platforms.`; + + const ogImage = release.albumCover + ? urlFor(release.albumCover).width(1200).height(1200).url() + : undefined; + + return { + title: `${release.albumArtist ? `${release.albumArtist} • ` : ""}${release.name}`, + description, + openGraph: { + title: release.name, + description: `${release.albumArtist || "TRPTK"}${release.label ? ` - ${release.label}` : ""}`, + type: "music.album", + ...(ogImage && { images: [{ url: ogImage, width: 1200, height: 1200, alt: `${release.name} by ${release.albumArtist}` }] }), + }, + twitter: { + card: "summary_large_image", + title: release.name, + description, + ...(ogImage && { images: [ogImage] }), + }, + }; +} + +export default async function ReleasePage({ params }: { params: Promise<{ slug: string }> }) { + const { slug } = await params; + if (!slug) notFound(); + + const normalizedSlug = slug.toLowerCase(); + if (slug !== normalizedSlug) { + redirect(`/${normalizedSlug}`); + } + + const release = await getRelease(slug); + if (!release) notFound(); + + const links = buildLinks(release); + const title = release.name ?? slug; + const albumArtist = release.albumArtist ?? ""; + + return ( +
+
+
+ + +
+ + +
+
+ + +
+
+ ); +} diff --git a/app/account/layout.tsx b/app/account/layout.tsx new file mode 100644 index 0000000..fe3828a --- /dev/null +++ b/app/account/layout.tsx @@ -0,0 +1,18 @@ +import { Header } from "@/components/header/Header"; +import { Footer } from "@/components/footer/Footer"; +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "My Account", + robots: { index: false, follow: false }, +}; + +export default function AccountLayout({ children }: { children: React.ReactNode }) { + return ( + <> +
+ {children} +