first version

This commit is contained in:
m4x809 2025-10-25 00:20:37 +02:00
commit 6633b7858f
Signed by: m4x809
SSH key fingerprint: SHA256:YCoFF78p2DUP94EnCScqLwldjkKDwdKSZq3r8p/6EiU
33 changed files with 1317 additions and 0 deletions

40
.gitignore vendored Normal file
View file

@ -0,0 +1,40 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
# bun
*.lock
.log

93
README.md Normal file
View file

@ -0,0 +1,93 @@
# Linkin Park Albums
A modern web application showcasing the complete discography of Linkin Park, built with Next.js 15 and React 19.
## Features
- 🎵 Browse all Linkin Park albums
- 📱 Responsive design with Tailwind CSS
- 🎨 Beautiful UI with Mantine components
- ⚡ Fast static generation with Next.js App Router
- 🖥️ Server components for optimal performance
- 🎭 Smooth transitions and hover effects
## Tech Stack
- **Framework**: Next.js 15 (App Router)
- **React**: React 19 (Canary)
- **UI Components**: Mantine Core 8.3.5
- **Styling**: Tailwind CSS 4.1.15
- **Package Manager**: Bun
- **TypeScript**: 5.9.3
## Project Structure
```
src/
├── app/
│ ├── layout.tsx # Root layout with Mantine provider
│ ├── page.tsx # Home page (server component)
│ └── album/
│ └── [albumId]/
│ ├── page.tsx # Album detail page (server component)
│ └── not-found.tsx
├── components/
│ ├── AlbumCard.tsx # Album card component (client component)
│ └── BackButton.tsx # Back button (client component)
├── lib/
│ ├── list.ts # Album data
│ └── ListTypes.ts # TypeScript types
└── index.css # Global styles
```
## Getting Started
Install dependencies:
```bash
bun install
```
Run the development server:
```bash
bun run dev
```
Open [http://localhost:3000](http://localhost:3000) to view the app.
## Build
Build for production:
```bash
bun run build
```
Start the production server:
```bash
bun run start
```
## Features Explanation
### Server Components
The app leverages Next.js App Router with server components for optimal performance:
- Home page (`page.tsx`) - Server component that renders the album grid
- Album detail page (`album/[albumId]/page.tsx`) - Server component with static generation
### Client Components
Interactive components use the `"use client"` directive:
- `AlbumCard` - Handles click navigation to album details
- `BackButton` - Handles navigation back to home
### Static Generation
Album detail pages are statically generated at build time using `generateStaticParams`, providing instant page loads.
## License
MIT

100
VIEW_TRANSITIONS_GUIDE.md Normal file
View file

@ -0,0 +1,100 @@
# View Transitions Implementation Guide
## Overview
This project now uses the native **View Transitions API** to create smooth animations between pages, including browser back/forward navigation. The implementation is based on best practices from the [nmn.sh](https://github.com/nmn/nmn.sh) repository.
## What Changed
### 1. CSS Configuration (`src/index.css`)
Added comprehensive view transition CSS rules:
```css
/* Enable smooth cross-document view transitions (browser navigation) */
@view-transition {
navigation: auto;
}
/* Default transition for all elements */
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 0.3s;
animation-timing-function: ease-in-out;
}
```
**Key Feature**: `@view-transition { navigation: auto; }` enables transitions to work with native browser navigation (back/forward buttons, URL changes), not just client-side routing.
### 2. Component Updates
**AlbumCard.tsx** and **album/[albumId]/page.tsx**:
Changed from React `ViewTransition` component wrapper:
```tsx
// ❌ Old approach (only works with client-side routing)
<ViewTransition name={`album-card-image-${album.id}`}>
<Image src={image} alt={label} />
</ViewTransition>
```
To native CSS `viewTransitionName` property:
```tsx
// ✅ New approach (works with all navigation)
<Image
src={image}
alt={label}
style={{ viewTransitionName: `album-card-image-${album.id}` }}
/>
```
### 3. TypeScript Support
Added type definitions in `src/types/view-transitions.d.ts` to extend React's `CSSProperties` interface with `viewTransitionName` support.
## How It Works
### Client-Side Routing
- Uses `next-view-transitions` package
- `<Link>` component from the package automatically triggers view transitions
- Smooth animations between pages when clicking links
### Browser Navigation (Back/Forward)
- Native `@view-transition { navigation: auto; }` CSS rule
- Browser automatically captures before/after states
- Smooth transitions even with browser buttons or URL changes
- **No JavaScript required** for basic transitions
### Named Transitions
Elements with matching `viewTransitionName` values will morph smoothly between pages:
- Album images: `album-card-image-{id}`
- Album titles: `album-card-title-{id}`
- Release dates: `album-card-release-date-{id}`
## Browser Support
- **Chrome/Edge**: Full support (v111+)
- **Safari**: Support in v18+ (macOS Sonoma, iOS 17)
- **Firefox**: In development
- **Fallback**: Graceful degradation to instant navigation
## Testing
1. **Client-side navigation**: Click any album card → smooth transition
2. **Browser back button**: Press back → smooth transition
3. **Direct URL change**: Type URL directly → smooth transition on supported browsers
## Benefits Over Previous Implementation
1. ✅ **Works with browser navigation** (back/forward buttons)
2. ✅ **Works with direct URL changes**
3. ✅ **Better performance** (native browser API)
4. ✅ **More reliable** (less JavaScript, more standards-based)
5. ✅ **Progressive enhancement** (works without JS)
## References
- [View Transitions API (MDN)](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API)
- [nmn.sh repository](https://github.com/nmn/nmn.sh)
- [Chrome View Transitions Guide](https://developer.chrome.com/docs/web-platform/view-transitions/)

37
biome.json Normal file
View file

@ -0,0 +1,37 @@
{
"$schema": "https://biomejs.dev/schemas/2.2.4/schema.json",
"assist": { "actions": { "source": { "organizeImports": "off" } } },
"formatter": {
"enabled": false
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"suspicious": {
"noExplicitAny": "off"
},
"style": {
"noNonNullAssertion": "off",
"useImportType": "warn",
"noParameterAssign": "off"
},
"a11y": {
"useKeyWithClickEvents": "off",
"useMediaCaption": "off",
"noSvgWithoutTitle": "off"
}
},
"includes": [
"**",
"!**/node_modules/**/*",
"!**/biome.json",
"!**/build/**/*",
"!**/.next/**/*",
"!**/drizzle/**/*",
"!**/*dockerignore*",
"!**/*.css",
"!**/*.log"
]
}
}

23
eslint.config.js Normal file
View file

@ -0,0 +1,23 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs['recommended-latest'],
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
},
])

9
next.config.ts Normal file
View file

@ -0,0 +1,9 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
viewTransition: true,
},
reactCompiler: true,
};
module.exports = nextConfig;

41
package.json Normal file
View file

@ -0,0 +1,41 @@
{
"name": "list-of-lp",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint ."
},
"dependencies": {
"@mantine/carousel": "^8.3.5",
"@mantine/core": "^8.3.5",
"@mantine/hooks": "^8.3.5",
"embla-carousel": "^8.5.2",
"embla-carousel-react": "^8.5.2",
"next": "^16.0.0",
"next-view-transitions": "^0.3.4",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"tailwindcss": "^4.1.15"
},
"devDependencies": {
"@eslint/js": "^9.36.0",
"@types/node": "^24.6.0",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2",
"babel-plugin-react-compiler": "^19.1.0-rc.3",
"eslint": "^9.36.0",
"eslint-plugin-react-hooks": "^5.2.0",
"globals": "^16.4.0",
"postcss": "^8.5.6",
"postcss-preset-mantine": "^1.18.0",
"postcss-simple-vars": "^7.0.1",
"prettier": "^3.6.2",
"prettier-plugin-tailwindcss": "^0.7.1",
"typescript": "~5.9.3",
"typescript-eslint": "^8.45.0"
}
}

14
postcss.config.js Normal file
View file

@ -0,0 +1,14 @@
export default {
plugins: {
"postcss-preset-mantine": {},
"postcss-simple-vars": {
variables: {
"mantine-breakpoint-xs": "36em",
"mantine-breakpoint-sm": "48em",
"mantine-breakpoint-md": "62em",
"mantine-breakpoint-lg": "75em",
"mantine-breakpoint-xl": "88em",
},
},
},
};

10
prettier.config.js Normal file
View file

@ -0,0 +1,10 @@
/** @type {import('prettier').Config & import('prettier-plugin-tailwindcss').PluginOptions} */
const config = {
plugins: ["prettier-plugin-tailwindcss"],
tabWidth: 1,
useTabs: true,
arrowParens: "always",
printWidth: 120,
};
export default config;

BIN
public/a_thousand_suns.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB

BIN
public/from_zero.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

BIN
public/from_zero_deluxe.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

BIN
public/hybrid_theory.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

BIN
public/living_things.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 556 KiB

BIN
public/lost_demos.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

BIN
public/meteora.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 478 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 375 KiB

BIN
public/one_more_light.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

BIN
public/papercuts.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

1
public/vite.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -0,0 +1,82 @@
"use client";
import { getThemeColors } from "@/lib/themes";
import type { Album } from "../lib/ListTypes";
import { Box, Card, Image, Text, Badge, Group } from "@mantine/core";
import { Link } from "next-view-transitions";
export default function AlbumCard({ album }: { album: Album }) {
const { label, releaseDate, image, tracks } = album;
type Songs = {
count: number;
emilyLiveSongs: number;
lpLiveSongs: number;
};
const songs: Songs = {
count: tracks.length,
emilyLiveSongs: tracks.filter((track) => track.emilyLiveUrl !== null).length,
lpLiveSongs: tracks.filter((track) => track.lpLiveUrl !== null).length,
};
const theme = getThemeColors(album.id);
return (
<Card
component={Link}
href={`/album/${album.id}`}
className="group col-span-1 cursor-pointer border border-gray-700 backdrop-blur-sm transition-all duration-300 hover:scale-105 hover:bg-gray-800/70 hover:shadow-2xl hover:shadow-black/20"
style={{
background: `linear-gradient(to bottom, ${theme.bg})`,
}}
>
<Box className="relative overflow-hidden rounded-lg">
<Image
src={image}
alt={label}
radius="md"
className="aspect-square w-full object-cover transition-transform duration-500 group-hover:scale-110"
style={{ viewTransitionName: `album-card-image-${album.id}` }}
/>
<Box className="absolute inset-0 bg-linear-to-t from-black/80 via-transparent to-transparent opacity-0 transition-opacity duration-300 group-hover:opacity-100" />
</Box>
<Box className="space-y-3 p-4">
<Text
size="xl"
fw={700}
className="text-white transition-colors group-hover:text-purple-300"
style={{ viewTransitionName: `album-card-title-${album.id}` }}
>
{label}
</Text>
<Text size="sm" className="text-gray-400" style={{ viewTransitionName: `album-card-release-date-${album.id}` }}>
Released:{" "}
{new Date(releaseDate).toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
})}
</Text>
<Group gap="xs">
<Badge color="purple" variant="light" size="sm" className="bg-purple-500/20 text-purple-300">
{songs.count} Songs
</Badge>
{songs.emilyLiveSongs > 0 && (
<Badge color="pink" variant="light" size="sm" className="bg-pink-500/20 text-pink-300">
{songs.emilyLiveSongs} Emily Live
</Badge>
)}
{songs.lpLiveSongs > 0 && (
<Badge color="blue" variant="light" size="sm" className="bg-blue-500/20 text-blue-300">
{songs.lpLiveSongs} LP Live
</Badge>
)}
</Group>
</Box>
</Card>
);
}

View file

@ -0,0 +1,18 @@
"use client";
import { Button } from "@mantine/core";
import { Link } from "next-view-transitions";
export default function BackButton() {
return (
<Button
component={Link}
href="/"
variant="subtle"
color="gray"
className="mb-8 text-gray-300 hover:bg-gray-800 hover:text-white"
>
Back to Albums
</Button>
);
}

View file

@ -0,0 +1,24 @@
import { Box, Container, Title, Text, Button } from "@mantine/core";
import Link from "next/link";
export default function NotFound() {
return (
<Box className="min-h-screen bg-gray-900">
<Container size="xl" className="flex min-h-screen items-center justify-center">
<Box className="text-center">
<Title order={1} className="mb-4 text-6xl font-bold text-white">
404
</Title>
<Text size="xl" className="mb-8 text-gray-400">
Album not found
</Text>
<Link href="/">
<Button variant="filled" color="purple" size="lg">
Back to Albums
</Button>
</Link>
</Box>
</Container>
</Box>
);
}

View file

@ -0,0 +1,196 @@
import { notFound } from "next/navigation";
import { Box, Container, Title, Text, Group, Badge, Card, Image, Stack, Grid, GridCol } from "@mantine/core";
import { albums } from "../../../lib/list";
import BackButton from "../../../Components/BackButton";
import { getThemeColors } from "@/lib/themes";
export function generateStaticParams() {
return albums.map((album) => ({
albumId: album.id,
}));
}
export async function generateMetadata({ params }: { params: Promise<{ albumId: string }> }) {
const { albumId } = await params;
const album = albums.find((a) => a.id === albumId);
if (!album) {
return {
title: "Album Not Found",
};
}
return {
title: `${album.label} - Linkin Park Albums`,
description: album.description,
};
}
export default async function AlbumDetail({ params }: { params: Promise<{ albumId: string }> }) {
const { albumId } = await params;
const album = albums.find((a) => a.id === albumId);
if (!album) {
notFound();
}
// Theme colors based on album
const theme = getThemeColors(albumId);
return (
<Box className="min-h-screen bg-gray-900">
<Container size="xl" className="py-12">
{/* Back Button */}
<BackButton />
<Grid columns={5} gutter={"xl"}>
<GridCol span={"content"}>
<Image
src={album.image}
alt={album.label}
h={200}
w={200}
className="aspect-square!"
style={{ viewTransitionName: `album-card-image-${album.id}` }}
/>
</GridCol>
<GridCol span={"auto"}>
<Title
order={1}
className="text-6xl font-bold text-white"
style={{ viewTransitionName: `album-card-title-${album.id}` }}
>
{album.label}
</Title>
<Text size="xl" className="text-gray-400" style={{ viewTransitionName: `album-card-release-date-${album.id}` }}>
Released:{" "}
{new Date(album.releaseDate).toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
})}
</Text>
<Text className="text-gray-400">{album.description}</Text>
</GridCol>
</Grid>
{/* Album Header */}
{/* <Box className="mb-12 grid grid-cols-1 gap-12 lg:grid-cols-3">
{/* Album Cover */}
{/* <Box className="lg:col-span-1">
<Card className="overflow-hidden border border-gray-700 bg-gray-800/50 backdrop-blur-sm">
<Image
src={album.image}
alt={album.label}
width={250}
height={250}
className="transition-all duration-700 ease-out"
/>
</Card>
</Box> */}
{/* Album Info */}
{/* <Box className="space-y-6 lg:col-span-2">
<Stack gap="md">
<Title order={1} className="text-6xl font-bold text-white">
{album.label}
</Title>
<Text size="xl" className="text-gray-400">
Released:{" "}
{new Date(album.releaseDate).toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
})}
</Text>
<Group gap="md">
<Badge
size="lg"
style={{
backgroundColor: theme.primary,
color: "white",
}}
>
{album.tracks.length} Songs
</Badge>
<Badge
size="lg"
variant="light"
style={{
backgroundColor: `${theme.secondary}20`,
color: theme.secondary,
borderColor: theme.secondary,
}}
>
Linkin Park
</Badge>
</Group>
</Stack>
<Divider color={theme.primary} />
<Text size="lg" className="leading-relaxed text-gray-300">
{album.description}
</Text>
</Box> */}
{/* </Box> */}
{/* Track List */}
{album.tracks.length > 0 && (
<Box>
<Title order={2} className="mb-6 text-3xl font-bold text-white">
Track List
</Title>
<Stack gap="sm">
{album.tracks.map((track, index) => (
<Card
key={track.id}
className="border border-gray-700 bg-gray-800/30 backdrop-blur-sm transition-all duration-200 hover:bg-gray-800/50"
>
<Group justify="space-between" align="center">
<Group gap="md">
<Text size="lg" fw={600} style={{ color: theme.primary }} className="min-w-8">
{index + 1}
</Text>
<Text size="lg" className="text-white">
{track.label}
</Text>
</Group>
<Group gap="md">
<Text size="sm" className="text-gray-400">
{track.duration}
</Text>
<Group gap="xs">
{track.studioUrl && (
<Badge size="sm" color="green" variant="light">
Studio
</Badge>
)}
{track.emilyLiveUrl && (
<Badge size="sm" color="pink" variant="light">
Emily Live
</Badge>
)}
{track.lpLiveUrl && (
<Badge size="sm" color="blue" variant="light">
LP Live
</Badge>
)}
</Group>
</Group>
</Group>
</Card>
))}
</Stack>
</Box>
)}
</Container>
</Box>
);
}

View file

@ -0,0 +1,34 @@
import { type NextRequest, NextResponse } from "next/server";
import { albums } from "../../../lib/list";
import { cookies } from "next/headers";
// Absolute URL redirect is required by Next.js middleware & API routes
export async function GET(request: NextRequest) {
const url = new URL(request.url);
const randParam = url.searchParams.get("rand");
const cookieStore = await cookies();
const lastImage = cookieStore.get("lastImage")?.value;
// Get all available albums, excluding the previous image
const availableAlbums = lastImage ? albums.filter((album) => album.image !== lastImage) : albums;
// Choose a random album, optionally seeded by the "rand" query param
let randomIndex: number;
if (randParam && !Number.isNaN(Number(randParam))) {
randomIndex = Number(randParam) % availableAlbums.length;
} else {
randomIndex = Math.floor(Math.random() * availableAlbums.length);
}
const album = availableAlbums[randomIndex];
// Ensure image path starts with a slash
const imagePath = album.image.startsWith("/") ? album.image : `/${album.image}`;
// Build absolute URL for the redirect
const { nextUrl } = request;
const absoluteUrl = `${nextUrl.protocol}//${nextUrl.host}${imagePath}`;
cookieStore.set("lastImage", album.image);
return NextResponse.redirect(absoluteUrl, 307);
}

34
src/app/layout.tsx Normal file
View file

@ -0,0 +1,34 @@
import "@mantine/core/styles.css";
import "../index.css";
import { MantineProvider, ColorSchemeScript } from "@mantine/core";
import { ViewTransitions } from "next-view-transitions";
import { Nunito } from "next/font/google";
const nunito = Nunito({
subsets: ["latin"],
weight: ["200", "300", "400", "500", "600", "700", "800", "900"],
});
export const metadata = {
title: "Linkin Park Albums",
description: "Explore the complete discography of Linkin Park",
icons: {
icon: "/api/favico",
},
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<ViewTransitions>
<html lang="en">
<head>
<meta name="darkreader-lock" />
{/* <ColorSchemeScript defaultColorScheme="dark" /> */}
</head>
<body className={nunito.className}>
<MantineProvider defaultColorScheme="dark">{children}</MantineProvider>
</body>
</html>
</ViewTransitions>
);
}

29
src/app/page.tsx Normal file
View file

@ -0,0 +1,29 @@
import { Box, Container, Title, Text, SimpleGrid } from "@mantine/core";
import AlbumCard from "../Components/AlbumCard";
import { albums } from "../lib/list";
export default function HomePage() {
return (
<Box className="min-h-screen bg-gray-900">
<Container size="xl" className="py-12">
<Box className="mb-12 text-center">
<Title
order={1}
className="mb-4 bg-linear-to-r from-purple-400 to-pink-400 bg-clip-text text-6xl font-bold text-transparent"
>
Linkin Park Albums
</Title>
<Text size="xl" className="text-gray-400">
Explore the complete discography of Linkin Park
</Text>
</Box>
<SimpleGrid cols={3} className="gap-8">
{albums.map((album) => (
<AlbumCard key={album.id} album={album} />
))}
</SimpleGrid>
</Container>
</Box>
);
}

91
src/index.css Normal file
View file

@ -0,0 +1,91 @@
@import "tailwindcss";
/* Dark mode base - prevent white flash */
* {
border-color: #1f2937;
}
html {
background-color: #111827;
scroll-behavior: smooth;
}
body {
background-color: #111827 !important;
color: white;
margin: 0;
padding: 0;
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.1);
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.3);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.5);
}
/* View Transitions API Configuration */
/* Enable smooth cross-document view transitions (browser navigation) */
@view-transition {
navigation: auto;
}
/* Default transition for all elements */
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 0.3s;
animation-timing-function: ease-in-out;
animation-fill-mode: both;
}
/* Fade transition for page root */
::view-transition-old(root) {
animation-name: fade-out;
}
::view-transition-new(root) {
animation-name: fade-in;
}
/* Custom transitions for specific elements with view-transition-name */
::view-transition-old(*),
::view-transition-new(*) {
animation-duration: 0.4s;
animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
animation-fill-mode: both;
}
/* Smooth image transitions */
::view-transition-image-pair(*) {
isolation: isolate;
}
/* Keyframes for fade animations */
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fade-out {
from {
opacity: 1;
}
to {
opacity: 0;
}
}

18
src/lib/ListTypes.ts Normal file
View file

@ -0,0 +1,18 @@
export type Track = {
id: string;
label: string;
duration: string;
studioUrl: string | null;
emilyLiveUrl: string | null;
lpLiveUrl: string | null;
};
export type Album = {
id: string;
label: string;
releaseDate: string;
image: string;
url: string;
description: string;
tracks: Track[];
};

333
src/lib/list.ts Normal file
View file

@ -0,0 +1,333 @@
import type { Album } from "./ListTypes";
export const albums: Album[] = [
{
id: "hybrid-theory",
label: "Hybrid Theory",
releaseDate: "2000-10-24",
image: "/hybrid_theory.jpg",
url: "/hybrid-theory",
description:
"Hybrid Theory is the debut studio album by American rock band Linkin Park, released on October 24, 2000, by Warner Bros. Records. It was recorded at The Plant Studios in Sausalito, California, and produced by Don Gilmore. The album was a commercial success, reaching number one on the Billboard 200 chart and selling over 10 million copies in the United States alone.",
tracks: [
{
id: "1",
label: "Papercut",
duration: "03:04",
studioUrl: null,
emilyLiveUrl: null,
lpLiveUrl: null,
},
{
id: "2",
label: "One Step Closer",
duration: "02:35",
studioUrl: null,
emilyLiveUrl: null,
lpLiveUrl: null,
},
{
id: "3",
label: "With You",
duration: "03:23",
studioUrl: null,
emilyLiveUrl: null,
lpLiveUrl: null,
},
{
id: "4",
label: "Points of Authority",
duration: "03:20",
studioUrl: null,
emilyLiveUrl: null,
lpLiveUrl: null,
},
{
id: "5",
label: "Crawling",
duration: "03:29",
studioUrl: null,
emilyLiveUrl: null,
lpLiveUrl: null,
},
{
id: "6",
label: "Runaway",
duration: "03:03",
studioUrl: null,
emilyLiveUrl: null,
lpLiveUrl: null,
},
{
id: "7",
label: "By Myself",
duration: "03:09",
studioUrl: null,
emilyLiveUrl: null,
lpLiveUrl: null,
},
{
id: "8",
label: "In the End",
duration: "03:36",
studioUrl: null,
emilyLiveUrl: null,
lpLiveUrl: null,
},
{
id: "9",
label: "A Place for My Head",
duration: "03:04",
studioUrl: null,
emilyLiveUrl: null,
lpLiveUrl: null,
},
{
id: "10",
label: "Forgotten",
duration: "03:04",
studioUrl: null,
emilyLiveUrl: null,
lpLiveUrl: null,
},
{
id: "11",
label: "Cure for the Itch",
duration: "02:37",
studioUrl: null,
emilyLiveUrl: null,
lpLiveUrl: null,
},
{
id: "12",
label: "Pushing Me Away",
duration: "03:11",
studioUrl: null,
emilyLiveUrl: null,
lpLiveUrl: null,
},
],
},
{
id: "meteora",
label: "Meteora",
releaseDate: "2003-03-24",
image: "/meteora.jpg",
url: "/meteora",
description:
"Meteora is the second studio album by American rock band Linkin Park, released on March 24, 2003, by Warner Bros. Records. It was recorded at The Plant Studios in Sausalito, California, and produced by Don Gilmore. The album was a commercial success, reaching number one on the Billboard 200 chart and selling over 10 million copies in the United States alone.",
tracks: [
{
id: "1",
label: "Foreword",
duration: "00:13",
studioUrl: null,
emilyLiveUrl: null,
lpLiveUrl: null,
},
{
id: "2",
label: "Don't Stay",
duration: "03:07",
studioUrl: null,
emilyLiveUrl: null,
lpLiveUrl: null,
},
{
id: "3",
label: "Somewhere I Belong",
duration: "03:33",
studioUrl: null,
emilyLiveUrl: null,
lpLiveUrl: null,
},
{
id: "4",
label: "Lying from You",
duration: "02:55",
studioUrl: null,
emilyLiveUrl: null,
lpLiveUrl: null,
},
{
id: "5",
label: "Hit the Floor",
duration: "02:44",
studioUrl: null,
emilyLiveUrl: null,
lpLiveUrl: null,
},
{
id: "6",
label: "Easier to Run",
duration: "03:24",
studioUrl: null,
emilyLiveUrl: null,
lpLiveUrl: null,
},
{
id: "7",
label: "Faint",
duration: "02:42",
studioUrl: null,
emilyLiveUrl: null,
lpLiveUrl: null,
},
{
id: "8",
label: "Figure.09",
duration: "03:17",
studioUrl: null,
emilyLiveUrl: null,
lpLiveUrl: null,
},
{
id: "9",
label: "Breaking the Habit",
duration: "03:16",
studioUrl: null,
emilyLiveUrl: null,
lpLiveUrl: null,
},
{
id: "10",
label: "From the Inside",
duration: "02:55",
studioUrl: null,
emilyLiveUrl: null,
lpLiveUrl: null,
},
{
id: "11",
label: "Nobody's Listening",
duration: "02:58",
studioUrl: null,
emilyLiveUrl: null,
lpLiveUrl: null,
},
{
id: "12",
label: "Session",
duration: "02:24",
studioUrl: null,
emilyLiveUrl: null,
lpLiveUrl: null,
},
{
id: "13",
label: "Numb",
duration: "03:05",
studioUrl: null,
emilyLiveUrl: null,
lpLiveUrl: null,
},
],
},
{
id: "minutes-to-midnight",
label: "Minutes to Midnight",
releaseDate: "2007-05-14",
image: "/minutes_to_midnight.jpg",
url: "/minutes-to-midnight",
description:
"Minutes to Midnight is the third studio album by American rock band Linkin Park, released on May 14, 2007, by Warner Bros. Records. The album marked a departure from the band's previous nu-metal sound, incorporating more alternative rock and experimental elements.",
tracks: [
{
id: "1",
label: "Wake",
duration: "01:40",
studioUrl: null,
emilyLiveUrl: null,
lpLiveUrl: null,
},
{
id: "2",
label: "Given Up",
duration: "03:09",
studioUrl: null,
emilyLiveUrl: null,
lpLiveUrl: null,
},
{
id: "3",
label: "Leave Out All the Rest",
duration: "03:29",
studioUrl: null,
emilyLiveUrl: null,
lpLiveUrl: null,
},
{
id: "4",
label: "Bleed It Out",
duration: "02:44",
studioUrl: null,
emilyLiveUrl: null,
lpLiveUrl: null,
},
{
id: "5",
label: "Shadow of the Day",
duration: "04:49",
studioUrl: null,
emilyLiveUrl: null,
lpLiveUrl: null,
},
{
id: "6",
label: "What I've Done",
duration: "03:25",
studioUrl: null,
emilyLiveUrl: null,
lpLiveUrl: null,
},
{
id: "7",
label: "Hands Held High",
duration: "03:53",
studioUrl: null,
emilyLiveUrl: null,
lpLiveUrl: null,
},
{
id: "8",
label: "No More Sorrow",
duration: "03:41",
studioUrl: null,
emilyLiveUrl: null,
lpLiveUrl: null,
},
{
id: "9",
label: "Valentine's Day",
duration: "03:16",
studioUrl: null,
emilyLiveUrl: null,
lpLiveUrl: null,
},
{
id: "10",
label: "In Between",
duration: "03:16",
studioUrl: null,
emilyLiveUrl: null,
lpLiveUrl: null,
},
{
id: "11",
label: "In Pieces",
duration: "03:38",
studioUrl: null,
emilyLiveUrl: null,
lpLiveUrl: null,
},
{
id: "12",
label: "The Little Things Give You Away",
duration: "06:23",
studioUrl: null,
emilyLiveUrl: null,
lpLiveUrl: null,
},
],
},
];

25
src/lib/themes.ts Normal file
View file

@ -0,0 +1,25 @@
export const getThemeColors = (albumId: string) => {
return themes[albumId] || themes["hybrid-theory"];
};
const themes: Record<string, { primary: string; secondary: string; accent: string; bg: string }> = {
"hybrid-theory": {
primary: "#ff6b35",
secondary: "#f7931e",
accent: "#ff1744",
bg: "from-orange-900 via-red-900 to-orange-800",
},
meteora: {
primary: "#8b5cf6",
secondary: "#a855f7",
accent: "#ec4899",
bg: "from-purple-900 via-pink-900 to-purple-800",
},
"minutes-to-midnight": {
primary: "#1e40af",
secondary: "#3b82f6",
accent: "#06b6d4",
bg: "from-slate-900 via-blue-900 to-slate-800",
},
};
export type Theme = (typeof themes)["hybrid-theory"];

26
src/types/view-transitions.d.ts vendored Normal file
View file

@ -0,0 +1,26 @@
// Type definitions for View Transitions API
// https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API
import "react";
declare module "react" {
interface CSSProperties {
viewTransitionName?: string;
}
}
// Extend global Document interface for view transitions
declare global {
interface Document {
startViewTransition?: (callback: () => void | Promise<void>) => ViewTransition;
}
interface ViewTransition {
finished: Promise<void>;
ready: Promise<void>;
updateCallbackDone: Promise<void>;
skipTransition: () => void;
}
}
export {};

39
tsconfig.json Normal file
View file

@ -0,0 +1,39 @@
{
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"allowJs": true,
"incremental": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"paths": {
"@/*": ["./src/*"]
},
"plugins": [
{
"name": "next"
}
],
"noEmit": true
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next\\dev/types/**/*.ts",
".next\\dev/types/**/*.ts"
],
"exclude": ["node_modules"]
}