first version
40
.gitignore
vendored
Normal 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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,9 @@
|
|||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
experimental: {
|
||||
viewTransition: true,
|
||||
},
|
||||
reactCompiler: true,
|
||||
};
|
||||
|
||||
module.exports = nextConfig;
|
||||
41
package.json
Normal 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
|
|
@ -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
|
|
@ -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
|
After Width: | Height: | Size: 208 KiB |
BIN
public/from_zero.jpg
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
public/from_zero_deluxe.jpg
Normal file
|
After Width: | Height: | Size: 106 KiB |
BIN
public/hybrid_theory.jpg
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
public/living_things.jpg
Normal file
|
After Width: | Height: | Size: 556 KiB |
BIN
public/lost_demos.jpg
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
BIN
public/meteora.jpg
Normal file
|
After Width: | Height: | Size: 478 KiB |
BIN
public/minutes_to_midnight.jpg
Normal file
|
After Width: | Height: | Size: 375 KiB |
BIN
public/one_more_light.jpg
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
public/papercuts.jpg
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
1
public/vite.svg
Normal 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 |
82
src/Components/AlbumCard.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
18
src/Components/BackButton.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
24
src/app/album/[albumId]/not-found.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
196
src/app/album/[albumId]/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
34
src/app/api/favico/route.tsx
Normal 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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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"]
|
||||
}
|
||||