├── .gitignore ├── .vscode ├── extensions.json └── launch.json ├── README.md ├── docs ├── assets │ ├── album-1-bfe26d94.js │ ├── album-2-6bcd8e89.js │ ├── album-3-e649e581.js │ ├── album-4-c4664fee.js │ ├── album-5-c168a9cb.js │ ├── album-6-3740d35b.js │ ├── album-7-9cf691a1.js │ ├── album-8-0ebba23b.js │ ├── albums-a0323168.js │ ├── index-3b3f9cb5.js │ └── index-7eb3bd97.css ├── favicon.svg ├── index.html ├── mp3 │ ├── song1.mp3 │ ├── song2.mp3 │ ├── song3.mp3 │ └── song4.mp3 └── vinyl-lp.webp ├── index.html ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── favicon.svg ├── mp3 │ ├── song1.mp3 │ ├── song2.mp3 │ ├── song3.mp3 │ └── song4.mp3 └── vinyl-lp.webp ├── screenshot.png ├── src ├── App.tsx ├── components │ ├── Card.tsx │ ├── Footer.tsx │ ├── Header.tsx │ ├── PlayButton.tsx │ ├── Player.tsx │ ├── Record.tsx │ ├── TrackList.tsx │ └── icons.tsx ├── data │ ├── album-1.json │ ├── album-2.json │ ├── album-3.json │ ├── album-4.json │ ├── album-5.json │ ├── album-6.json │ ├── album-7.json │ ├── album-8.json │ └── albums.json ├── index.css ├── main.jsx └── routes │ ├── Album.tsx │ ├── Home.tsx │ └── Layout.tsx ├── tailwind.config.js ├── tsconfig.json └── vite.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | # build output 2 | .output/ 3 | 4 | # dependencies 5 | node_modules/ 6 | 7 | # logs 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | pnpm-debug.log* 12 | 13 | 14 | # environment variables 15 | .env 16 | .env.production 17 | 18 | # macOS-specific files 19 | .DS_Store 20 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["astro-build.astro-vscode"], 3 | "unwantedRecommendations": [] 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "command": "./node_modules/.bin/astro dev", 6 | "name": "Development server", 7 | "request": "launch", 8 | "type": "node-terminal" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Router View Transitions Demo 2 | 3 | ## ❤️ Credits 4 | 5 | This is a fork of the awesome [Astro Records](https://github.com/Charca/astro-records) Demo (using Astro's view transition support) that has been converted to use React Router's view transition APIs. 6 | 7 | ## 👉🏽 [Live Demo](https://brophdawg11.github.io/react-router-records/) 8 | 9 | ![Screenshot](./screenshot.png) 10 | 11 | ## 🚀 Getting Started 12 | 13 | 1. Clone this repository and install dependencies with `npm ci`. 14 | 2. Start the project locally with `npm run dev` 15 | 3. Have fun! ✨ 16 | -------------------------------------------------------------------------------- /docs/assets/album-1-bfe26d94.js: -------------------------------------------------------------------------------- 1 | const t="1",i="Hot Fuss",e="The Killers",n="https://www.theaudiodb.com/images/media/album/thumb/hot-fuss-limited-edition-7-inch-box-set-4ddc38e3e1d71.jpg",o=[{id:"1-1",position:1,title:"Jenny Was a Friend of Mine",length:"4:05"},{id:"1-2",position:2,title:"Mr. Brightside",length:"3:43"},{id:"1-3",position:3,title:"Smile Like You Mean It",length:"3:55"},{id:"1-4",position:4,title:"Somebody Told Me",length:"3:17"},{id:"1-5",position:5,title:"All These Things That I've Done",length:"5:02"},{id:"1-6",position:6,title:"Andy, You're a Star",length:"3:14"},{id:"1-7",position:7,title:"On Top",length:"4:19"},{id:"1-8",position:8,title:"Change Your Mind",length:"3:11"},{id:"1-9",position:9,title:"Believe Me Natalie",length:"5:07"},{id:"1-10",position:10,title:"Midnight Show",length:"4:03"},{id:"1-11",position:11,title:"Everything Will Be Alright",length:"5:45"}],l={id:t,name:i,artist:e,img:n,tracks:o};export{e as artist,l as default,t as id,n as img,i as name,o as tracks}; 2 | -------------------------------------------------------------------------------- /docs/assets/album-2-6bcd8e89.js: -------------------------------------------------------------------------------- 1 | const t="2",i="First Impressions of Earth",e="The Strokes",n="https://www.theaudiodb.com/images/media/album/thumb/rqsryv1477576489.jpg",o=[{id:"2-1",position:1,title:"You Only Live Once",length:"3:09"},{id:"2-2",position:2,title:"Juicebox",length:"3:18"},{id:"2-3",position:3,title:"Heart in a Cage",length:"3:27"},{id:"2-4",position:4,title:"Razorblade",length:"3:29"},{id:"2-5",position:5,title:"On the Other Side",length:"4:39"},{id:"2-6",position:6,title:"Vision of Division",length:"4:20"},{id:"2-7",position:7,title:"Ask Me Anything",length:"3:13"},{id:"2-8",position:8,title:"Electricityscape",length:"3:33"},{id:"2-9",position:9,title:"Killing Lies",length:"3:50"},{id:"2-10",position:10,title:"Fear of Sleep",length:"4:00"},{id:"2-11",position:11,title:"15 Minutes",length:"4:34"},{id:"2-12",position:12,title:"Ize of the World",length:"4:29"},{id:"2-13",position:13,title:"Evening Sun",length:"3:06"},{id:"2-14",position:14,title:"Red Light",length:"3:12"}],s={id:t,name:i,artist:e,img:n,tracks:o};export{e as artist,s as default,t as id,n as img,i as name,o as tracks}; 2 | -------------------------------------------------------------------------------- /docs/assets/album-3-e649e581.js: -------------------------------------------------------------------------------- 1 | const t="3",i="Inside In/Inside Out",e="The Kooks",o="https://www.theaudiodb.com/images/media/album/thumb/inside-ininside-out-4e5608971ceb2.jpeg",n=[{id:"3-1",position:1,title:"Seaside",length:"1:39"},{id:"3-2",position:2,title:"See the World",length:"2:38"},{id:"3-3",position:3,title:"Sofa Song",length:"2:13"},{id:"3-4",position:4,title:"Eddie’s Gun",length:"2:13"},{id:"3-5",position:5,title:"Ooh La",length:"3:29"},{id:"3-6",position:6,title:"You Don’t Love Me",length:"2:35"},{id:"3-7",position:7,title:"She Moves in Her Own Way",length:"2:49"},{id:"3-8",position:8,title:"Matchbox",length:"3:10"},{id:"3-9",position:9,title:"Naïve",length:"3:24"},{id:"3-10",position:10,title:"I Want You",length:"3:27"},{id:"3-11",position:11,title:"If Only",length:"2:01"},{id:"3-12",position:12,title:"Jackie Big Tees",length:"2:33"},{id:"3-13",position:13,title:"Time Awaits",length:"5:09"},{id:"3-14",position:14,title:"Got No Love",length:"3:38"}],s={id:t,name:i,artist:e,img:o,tracks:n};export{e as artist,s as default,t as id,o as img,i as name,n as tracks}; 2 | -------------------------------------------------------------------------------- /docs/assets/album-4-c4664fee.js: -------------------------------------------------------------------------------- 1 | const t="4",i="Favourite Worst Nightmare",e="Arctic Monkeys",o="https://www.theaudiodb.com/images/media/album/thumb/favourite-worst-nightmare-4e612d12273ea.jpg",n=[{id:"4-1",position:1,title:"Brianstorm",length:"2:50"},{id:"4-2",position:2,title:"Teddy Picker",length:"2:43"},{id:"4-3",position:3,title:"D Is for Dangerous",length:"2:16"},{id:"4-4",position:4,title:"Balaclava",length:"2:49"},{id:"4-5",position:5,title:"Fluorescent Adolescent",length:"2:58"},{id:"4-6",position:6,title:"Only Ones Who Know",length:"3:03"},{id:"4-7",position:7,title:"Do Me a Favour",length:"3:27"},{id:"4-8",position:8,title:"This House Is a Circus",length:"3:10"},{id:"4-9",position:9,title:"If You Were There, Beware",length:"4:34"},{id:"4-10",position:10,title:"The Bad Thing",length:"2:23"},{id:"4-11",position:11,title:"Old Yellow Bricks",length:"3:11"},{id:"4-12",position:12,title:"505",length:"4:14"}],s={id:t,name:i,artist:e,img:o,tracks:n};export{e as artist,s as default,t as id,o as img,i as name,n as tracks}; 2 | -------------------------------------------------------------------------------- /docs/assets/album-5-c168a9cb.js: -------------------------------------------------------------------------------- 1 | const t="5",i="Up All Night",l="Razorlight",o="https://www.theaudiodb.com/images/media/album/thumb/uxputq1399632252.jpg",e=[{id:"5-1",position:1,title:"Leave Me Alone",length:"3:50"},{id:"5-2",position:2,title:"Rock n Roll Lies",length:"3:08"},{id:"5-3",position:3,title:"Vice",length:"3:14"},{id:"5-4",position:4,title:"Up All Night",length:"4:03"},{id:"5-5",position:5,title:"Which Way Is Out",length:"3:19"},{id:"5-6",position:6,title:"Rip It Up",length:"2:25"},{id:"5-7",position:7,title:"Don’t Go Back to Dalston",length:"2:59"},{id:"5-8",position:8,title:"Golden Touch",length:"3:25"},{id:"5-9",position:9,title:"Stumble and Fall",length:"3:02"},{id:"5-10",position:10,title:"Get It and Go",length:"3:23"},{id:"5-11",position:11,title:"In the City",length:"4:51"},{id:"5-12",position:12,title:"To the Sea",length:"5:31"},{id:"5-13",position:13,title:"Fall, Fall, Fall",length:"2:43"}],n={id:t,name:i,artist:l,img:o,tracks:e};export{l as artist,n as default,t as id,o as img,i as name,e as tracks}; 2 | -------------------------------------------------------------------------------- /docs/assets/album-6-3740d35b.js: -------------------------------------------------------------------------------- 1 | const t="6",i="Phrazes for the Young",o="Julian Casablancas",e="https://www.theaudiodb.com/images/media/album/thumb/phrazes-for-the-young-52f7faa979a19.jpg",s=[{id:"6-1",position:1,title:"Out of the Blue",length:"4:43"},{id:"6-2",position:2,title:"Left & Right in the Dark",length:"4:59"},{id:"6-3",position:3,title:"11th Dimension",length:"4:06"},{id:"6-4",position:4,title:"4 Chords of the Apocalypse",length:"5:02"},{id:"6-5",position:5,title:"Ludlow St.",length:"5:45"},{id:"6-6",position:6,title:"River of Brakelights",length:"5:13"},{id:"6-7",position:7,title:"Glass",length:"5:25"},{id:"6-8",position:8,title:"Tourist",length:"5:03"}],n={id:t,name:i,artist:o,img:e,tracks:s};export{o as artist,n as default,t as id,e as img,i as name,s as tracks}; 2 | -------------------------------------------------------------------------------- /docs/assets/album-7-9cf691a1.js: -------------------------------------------------------------------------------- 1 | const t="7",i="Franz Ferdinand",e="Franz Ferdinand",n="https://www.theaudiodb.com/images/media/album/thumb/franz-ferdinand-4dd465083a9a8.jpg",o=[{id:"7-1",position:1,title:"Jacqueline",length:"3:49"},{id:"7-2",position:2,title:"Tell Her Tonight",length:"2:18"},{id:"7-3",position:3,title:"Take Me Out",length:"3:57"},{id:"7-4",position:4,title:"The Dark of the Matinée",length:"4:03"},{id:"7-5",position:5,title:"Auf Achse",length:"4:20"},{id:"7-6",position:6,title:"Cheating on You",length:"2:37"},{id:"7-7",position:7,title:"This Fire",length:"4:15"},{id:"7-8",position:8,title:"Darts of Pleasure",length:"3:00"},{id:"7-9",position:9,title:"Michael",length:"3:21"},{id:"7-10",position:10,title:"Come On Home",length:"3:46"},{id:"7-11",position:11,title:"40′",length:"3:23"}],l={id:t,name:i,artist:e,img:n,tracks:o};export{e as artist,l as default,t as id,n as img,i as name,o as tracks}; 2 | -------------------------------------------------------------------------------- /docs/assets/album-8-0ebba23b.js: -------------------------------------------------------------------------------- 1 | const t="8",i="Get Born",e="Jet",o="https://www.theaudiodb.com/images/media/album/thumb/get-born-4ddedb9009071.jpg",n=[{id:"8-1",position:1,title:"Last Chance",length:"1:52"},{id:"8-2",position:2,title:"Are You Gonna Be My Girl",length:"3:34"},{id:"8-3",position:3,title:"Rollover D.J.",length:"3:17"},{id:"8-4",position:4,title:"Look What You’ve Done",length:"3:51"},{id:"8-5",position:5,title:"Get What You Need",length:"4:08"},{id:"8-6",position:6,title:"Move On",length:"4:21"},{id:"8-7",position:7,title:"Radio Song",length:"4:32"},{id:"8-8",position:8,title:"Get Me Outta Here",length:"2:56"},{id:"8-9",position:9,title:"Cold Hard Peach",length:"4:03"},{id:"8-10",position:10,title:"Come Around Again",length:"4:30"},{id:"8-11",position:11,title:"Take It or Leave It",length:"2:23"},{id:"8-12",position:12,title:"Lazy Gun",length:"4:42"},{id:"8-13",position:13,title:"Timothy",length:"4:30"}],l={id:t,name:i,artist:e,img:o,tracks:n};export{e as artist,l as default,t as id,o as img,i as name,n as tracks}; 2 | -------------------------------------------------------------------------------- /docs/assets/albums-a0323168.js: -------------------------------------------------------------------------------- 1 | const t=[{id:"1",name:"Hot Fuss",artist:"The Killers",img:"https://www.theaudiodb.com/images/media/album/thumb/hot-fuss-limited-edition-7-inch-box-set-4ddc38e3e1d71.jpg"},{id:"2",name:"First Impressions of Earth",artist:"The Strokes",img:"https://www.theaudiodb.com/images/media/album/thumb/rqsryv1477576489.jpg"},{id:"3",name:"Inside In/Inside Out",artist:"The Kooks",img:"https://www.theaudiodb.com/images/media/album/thumb/inside-ininside-out-4e5608971ceb2.jpeg"},{id:"4",name:"Favourite Worst Nightmare",artist:"Arctic Monkeys",img:"https://www.theaudiodb.com/images/media/album/thumb/favourite-worst-nightmare-4e612d12273ea.jpg"},{id:"5",name:"Up All Night",artist:"Razorlight",img:"https://www.theaudiodb.com/images/media/album/thumb/uxputq1399632252.jpg"},{id:"6",name:"Phrazes for the Young",artist:"Julian Casablancas",img:"https://www.theaudiodb.com/images/media/album/thumb/phrazes-for-the-young-52f7faa979a19.jpg"},{id:"7",name:"Franz Ferdinand",artist:"Franz Ferdinand",img:"https://www.theaudiodb.com/images/media/album/thumb/franz-ferdinand-4dd465083a9a8.jpg"},{id:"8",name:"Get Botn",artist:"Jet",img:"https://www.theaudiodb.com/images/media/album/thumb/get-born-4ddedb9009071.jpg"}];export{t as default}; 2 | -------------------------------------------------------------------------------- /docs/assets/index-7eb3bd97.css: -------------------------------------------------------------------------------- 1 | *,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.container{width:100%}@media (min-width: 640px){.container{max-width:640px}}@media (min-width: 768px){.container{max-width:768px}}@media (min-width: 1024px){.container{max-width:1024px}}@media (min-width: 1280px){.container{max-width:1280px}}@media (min-width: 1536px){.container{max-width:1536px}}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.bottom-0{bottom:0}.left-0{left:0}.right-0{right:0}.top-0{top:0}.z-10{z-index:10}.mx-auto{margin-left:auto;margin-right:auto}.-ml-1{margin-left:-.25rem}.mb-10{margin-bottom:2.5rem}.mb-12{margin-bottom:3rem}.ml-auto{margin-left:auto}.mr-2{margin-right:.5rem}.mr-32{margin-right:8rem}.mr-4{margin-right:1rem}.mt-0{margin-top:0}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.grid{display:grid}.hidden{display:none}.h-1{height:.25rem}.h-1\.5{height:.375rem}.h-10{height:2.5rem}.h-6{height:1.5rem}.w-10{width:2.5rem}.w-6{width:1.5rem}.w-72{width:18rem}.w-8{width:2rem}.w-full{width:100%}.min-w-0{min-width:0px}.max-w-screen-lg{max-width:1024px}.flex-1{flex:1 1 0%}.cursor-pointer{cursor:pointer}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-end{justify-content:flex-end}.gap-5{gap:1.25rem}.gap-6{gap:1.5rem}.overflow-hidden{overflow:hidden}.text-ellipsis{text-overflow:ellipsis}.whitespace-nowrap{white-space:nowrap}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.border-b{border-bottom-width:1px}.bg-gray-100{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity))}.bg-gray-200{--tw-bg-opacity: 1;background-color:rgb(229 231 235 / var(--tw-bg-opacity))}.bg-pink-500{--tw-bg-opacity: 1;background-color:rgb(236 72 153 / var(--tw-bg-opacity))}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity))}.px-10{padding-left:2.5rem;padding-right:2.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.py-8{padding-top:2rem;padding-bottom:2rem}.pb-12{padding-bottom:3rem}.pb-32{padding-bottom:8rem}.pl-2{padding-left:.5rem}.pt-1{padding-top:.25rem}.pt-4{padding-top:1rem}.pt-8{padding-top:2rem}.text-center{text-align:center}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-5xl{font-size:3rem;line-height:1}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.leading-normal{line-height:1.5}.tracking-normal{letter-spacing:0em}.tracking-tight{letter-spacing:-.025em}.text-black{--tw-text-opacity: 1;color:rgb(0 0 0 / var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity))}.text-gray-600{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity))}.text-gray-700{--tw-text-opacity: 1;color:rgb(55 65 81 / var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity: 1;color:rgb(17 24 39 / var(--tw-text-opacity))}.text-pink-600{--tw-text-opacity: 1;color:rgb(219 39 119 / var(--tw-text-opacity))}.underline{text-decoration-line:underline}.no-underline{text-decoration-line:none}.opacity-0{opacity:0}.shadow-md{--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / .1), 0 2px 4px -2px rgb(0 0 0 / .1);--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-xl{--tw-shadow: 0 20px 25px -5px rgb(0 0 0 / .1), 0 8px 10px -6px rgb(0 0 0 / .1);--tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.vinyl-animation-in{animation:appear .6s ease-in-out .3s forwards}.vinyl-animation-in-spinning{animation:appear .6s ease-in-out .3s forwards,spin 5s linear infinite}.vinyl-animation-out{animation:disappear .3s ease-in-out forwards}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}@keyframes appear{0%{left:0;opacity:0}1%{opacity:1}to{left:24%;opacity:1}}@keyframes disappear{0%{left:24%;opacity:1}99%{opacity:1}to{left:0;opacity:0}}a.transitioning .c-card--album,.c-record--album{view-transition-name:album-expand}::view-transition-old(album-expand):not(:only-child),::view-transition-new(album-expand):not(:only-child){animation:none;mix-blend-mode:normal}a.transitioning .c-card--vinyl,.c-record--vinyl{view-transition-name:vinyl-expand}::view-transition-old(vinyl-expand):not(:only-child),::view-transition-new(vinyl-expand):not(:only-child){animation:none;mix-blend-mode:normal}.c-player{view-transition-name:"player"}::view-transition-old(player){display:none}::view-transition-new(player){animation:none}.first\:border-t:first-child{border-top-width:1px}.hover\:bg-gray-200:hover{--tw-bg-opacity: 1;background-color:rgb(229 231 235 / var(--tw-bg-opacity))}.hover\:bg-gray-50:hover{--tw-bg-opacity: 1;background-color:rgb(249 250 251 / var(--tw-bg-opacity))}.hover\:text-pink-500:hover{--tw-text-opacity: 1;color:rgb(236 72 153 / var(--tw-text-opacity))}.hover\:no-underline:hover{text-decoration-line:none}.hover\:shadow-lg:hover{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-4:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-gray-100:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(243 244 246 / var(--tw-ring-opacity))}@media (prefers-color-scheme: dark){.dark\:bg-gray-700{--tw-bg-opacity: 1;background-color:rgb(55 65 81 / var(--tw-bg-opacity))}.dark\:focus\:ring-gray-500:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(107 114 128 / var(--tw-ring-opacity))}}@media (min-width: 640px){.sm\:block{display:block}.sm\:h-14{height:3.5rem}.sm\:w-14{width:3.5rem}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:px-6{padding-left:1.5rem;padding-right:1.5rem}.sm\:py-4{padding-top:1rem;padding-bottom:1rem}}@media (min-width: 768px){.md\:w-auto{width:auto}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.md\:flex-row{flex-direction:row}.md\:items-end{align-items:flex-end}.md\:px-0{padding-left:0;padding-right:0}}@media (min-width: 1024px){.lg\:w-1\/2{width:50%}.lg\:px-0{padding-left:0;padding-right:0}} 2 | -------------------------------------------------------------------------------- /docs/favicon.svg: -------------------------------------------------------------------------------- 1 | 8 | 12 | 16 | 20 | 24 | 25 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | React Router View Transitions Demo 9 | 10 | 11 | 12 | 15 |
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /docs/mp3/song1.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brophdawg11/react-router-records/d4defe0317dabf14ac435a17b1e49ad8ec4fc786/docs/mp3/song1.mp3 -------------------------------------------------------------------------------- /docs/mp3/song2.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brophdawg11/react-router-records/d4defe0317dabf14ac435a17b1e49ad8ec4fc786/docs/mp3/song2.mp3 -------------------------------------------------------------------------------- /docs/mp3/song3.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brophdawg11/react-router-records/d4defe0317dabf14ac435a17b1e49ad8ec4fc786/docs/mp3/song3.mp3 -------------------------------------------------------------------------------- /docs/mp3/song4.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brophdawg11/react-router-records/d4defe0317dabf14ac435a17b1e49ad8ec4fc786/docs/mp3/song4.mp3 -------------------------------------------------------------------------------- /docs/vinyl-lp.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brophdawg11/react-router-records/d4defe0317dabf14ac435a17b1e49ad8ec4fc786/docs/vinyl-lp.webp -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | React Router View Transitions Demo 9 | 10 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-router-records", 3 | "private": true, 4 | "version": "0.0.1", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "react": "^18.2.0", 14 | "react-dom": "^18.2.0", 15 | "react-router-dom": "^6.17.0-pre.2" 16 | }, 17 | "devDependencies": { 18 | "@types/react": "^18.2.15", 19 | "@types/react-dom": "^18.2.7", 20 | "@vitejs/plugin-react": "^4.0.3", 21 | "autoprefixer": "^10.4.16", 22 | "eslint": "^8.45.0", 23 | "eslint-plugin-react": "^7.32.2", 24 | "eslint-plugin-react-hooks": "^4.6.0", 25 | "eslint-plugin-react-refresh": "^0.4.3", 26 | "postcss": "^8.4.31", 27 | "tailwindcss": "^3.3.3", 28 | "vite": "^4.4.5" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | 8 | 12 | 16 | 20 | 24 | 25 | -------------------------------------------------------------------------------- /public/mp3/song1.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brophdawg11/react-router-records/d4defe0317dabf14ac435a17b1e49ad8ec4fc786/public/mp3/song1.mp3 -------------------------------------------------------------------------------- /public/mp3/song2.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brophdawg11/react-router-records/d4defe0317dabf14ac435a17b1e49ad8ec4fc786/public/mp3/song2.mp3 -------------------------------------------------------------------------------- /public/mp3/song3.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brophdawg11/react-router-records/d4defe0317dabf14ac435a17b1e49ad8ec4fc786/public/mp3/song3.mp3 -------------------------------------------------------------------------------- /public/mp3/song4.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brophdawg11/react-router-records/d4defe0317dabf14ac435a17b1e49ad8ec4fc786/public/mp3/song4.mp3 -------------------------------------------------------------------------------- /public/vinyl-lp.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brophdawg11/react-router-records/d4defe0317dabf14ac435a17b1e49ad8ec4fc786/public/vinyl-lp.webp -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brophdawg11/react-router-records/d4defe0317dabf14ac435a17b1e49ad8ec4fc786/screenshot.png -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { RouterProvider, createBrowserRouter } from "react-router-dom"; 2 | import Layout from "./routes/Layout"; 3 | import { 4 | Component as HomeComponent, 5 | loader as homeLoader, 6 | } from "./routes/Home"; 7 | import { 8 | Component as AlbumComponent, 9 | loader as albumLoader, 10 | } from "./routes/Album"; 11 | 12 | let router = createBrowserRouter( 13 | [ 14 | { 15 | path: "/", 16 | Component: Layout, 17 | children: [ 18 | { 19 | index: true, 20 | loader: homeLoader, 21 | Component: HomeComponent, 22 | }, 23 | { 24 | path: "album/:id", 25 | loader: albumLoader, 26 | Component: AlbumComponent, 27 | }, 28 | ], 29 | }, 30 | ], 31 | { 32 | basename: "/react-router-records", 33 | } 34 | ); 35 | 36 | export default function App() { 37 | return ; 38 | } 39 | -------------------------------------------------------------------------------- /src/components/Card.tsx: -------------------------------------------------------------------------------- 1 | import { NavLink } from "react-router-dom"; 2 | 3 | type CardProps = { 4 | id: string; 5 | name: string; 6 | artist: string; 7 | imageUrl: string; 8 | }; 9 | 10 | export function Card({ id, name, artist, imageUrl }: CardProps) { 11 | return ( 12 |
13 | 18 |
19 | {name} 26 | 32 |
33 |

{name}

34 |

{artist}

35 |
36 |
37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /src/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | export default function Footer() { 2 | return ( 3 |
4 |
5 |
6 |
7 |
8 |

About

9 |

10 | This site is a demo of a React Router SPA using{" "} 11 | React Router 6.17 with the experimental support 12 | for the View Transitions API ✨ 13 |

14 |

15 | Huge thanks and shout out to original{" "} 16 | 22 | Astro Records 23 | {" "} 24 | demo which this is based on 🙌 25 |

26 |

27 | Made with ❤️ by{" "} 28 | 34 | Matt Brophy 35 | {" "} 36 | ( 37 | 43 | Github Repo 44 | 45 | ) 46 |

47 |
48 |
49 |
50 |
51 |
52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /src/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | 3 | export default function Header() { 4 | return ( 5 | 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /src/components/PlayButton.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { PlayerContext, Track } from "../routes/Layout"; 3 | import { CirclePauseIcon, CirclePlayIcon } from "./icons"; 4 | 5 | type PlayButtonProps = { 6 | tracks: Track[]; 7 | albumId: string; 8 | artist: string; 9 | imageUrl: string; 10 | }; 11 | 12 | export default function PlayButton({ 13 | tracks, 14 | albumId, 15 | artist, 16 | imageUrl, 17 | }: PlayButtonProps) { 18 | let { isPlaying, setIsPlaying, currentTrack, setCurrentTrack } = 19 | React.useContext(PlayerContext); 20 | const isPlayingCurrentRecord = isPlaying && currentTrack?.albumId === albumId; 21 | 22 | return ( 23 | 53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /src/components/Player.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { PlayerContext } from "../routes/Layout"; 3 | 4 | const PlayIcon = ( 5 | 11 | 16 | 17 | ); 18 | 19 | const PauseIcon = ( 20 | 26 | 31 | 32 | ); 33 | 34 | // This app doesn't have real songs, it only has a few songs 35 | // that we will play over and over as the user uses the app. 36 | const MAX_SONGS = 4; 37 | 38 | export default function Player() { 39 | let audioPlayer = React.useRef(null); 40 | let progressRef = React.useRef(0); 41 | let [songIndex, setSongIndex] = React.useState(4); 42 | let [progress, setProgress] = React.useState(0); 43 | let { isPlaying, setIsPlaying, currentTrack } = 44 | React.useContext(PlayerContext); 45 | 46 | function whilePlaying() { 47 | if (!audioPlayer.current) return; 48 | if (audioPlayer.current.duration) { 49 | const percentage = 50 | (audioPlayer.current.currentTime * 100) / audioPlayer.current.duration; 51 | setProgress(percentage); 52 | } 53 | progressRef.current = requestAnimationFrame(whilePlaying); 54 | } 55 | 56 | React.useEffect(() => { 57 | if (!audioPlayer.current) return; 58 | const newIndex = (songIndex % MAX_SONGS) + 1; 59 | audioPlayer.current.src = `/react-router-records/mp3/song${newIndex}.mp3`; 60 | audioPlayer.current.currentTime = 0; 61 | audioPlayer.current.play(); 62 | setSongIndex(newIndex); 63 | }, [currentTrack?.title]); 64 | 65 | React.useEffect(() => { 66 | if (isPlaying) { 67 | audioPlayer.current?.play(); 68 | progressRef.current = requestAnimationFrame(whilePlaying); 69 | } else { 70 | audioPlayer.current?.pause(); 71 | cancelAnimationFrame(progressRef.current); 72 | } 73 | }, [isPlaying]); 74 | 75 | React.useEffect(() => { 76 | if (progress >= 99.99) { 77 | setIsPlaying(false); 78 | setProgress(0); 79 | } 80 | }, [progress]); 81 | 82 | if (currentTrack === null) { 83 | return null; 84 | } 85 | 86 | return ( 87 |
88 |
89 |
93 |
94 |
95 | 101 |
102 |

103 | {currentTrack.title} 104 |

105 |

106 | {currentTrack.artist} 107 |

108 |
109 |
133 |
134 | ); 135 | } 136 | -------------------------------------------------------------------------------- /src/components/Record.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { PlayerContext } from "../routes/Layout"; 3 | 4 | type RecordProps = { 5 | albumId: string; 6 | title: string; 7 | imageUrl: string; 8 | }; 9 | 10 | export default function Record({ albumId, title, imageUrl }: RecordProps) { 11 | let { isPlaying, currentTrack } = React.useContext(PlayerContext); 12 | const isPlayingCurrentRecord = isPlaying && currentTrack?.albumId === albumId; 13 | const animationClass = isPlayingCurrentRecord 14 | ? "vinyl-animation-in-spinning" 15 | : "vinyl-animation-in"; 16 | 17 | return ( 18 |
19 | {title} 26 | 32 |
33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /src/components/TrackList.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { PlayerContext, Track } from "../routes/Layout"; 3 | import { CirclePauseIcon, CirclePlayIcon } from "./icons"; 4 | 5 | type TrackListProps = { 6 | tracks: Track[]; 7 | albumId: string; 8 | artist: string; 9 | imageUrl: string; 10 | }; 11 | 12 | export default function TrackList({ 13 | tracks, 14 | albumId, 15 | artist, 16 | imageUrl, 17 | }: TrackListProps) { 18 | let { isPlaying, setIsPlaying, currentTrack, setCurrentTrack } = 19 | React.useContext(PlayerContext); 20 | 21 | return ( 22 |
    23 | {tracks.map((track) => { 24 | const isCurrentTrack = track.id == currentTrack?.id; 25 | 26 | return ( 27 |
  • { 31 | setCurrentTrack({ 32 | ...track, 33 | albumId, 34 | artist, 35 | imageUrl, 36 | }); 37 | setIsPlaying(true); 38 | }} 39 | > 40 | 41 | {isCurrentTrack && !isPlaying ? ( 42 | 43 | ) : isCurrentTrack && isPlaying ? ( 44 | 45 | ) : ( 46 | track.position 47 | )} 48 | 49 | {track.title} 50 | {track.length} 51 |
  • 52 | ); 53 | })} 54 |
55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /src/components/icons.tsx: -------------------------------------------------------------------------------- 1 | export function CirclePlayIcon() { 2 | return ( 3 | 16 | ); 17 | } 18 | 19 | export function CirclePauseIcon() { 20 | return ( 21 | 27 | 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /src/data/album-1.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "1", 3 | "name": "Hot Fuss", 4 | "artist": "The Killers", 5 | "img": "https://www.theaudiodb.com/images/media/album/thumb/hot-fuss-limited-edition-7-inch-box-set-4ddc38e3e1d71.jpg", 6 | "tracks": [ 7 | { 8 | "id": "1-1", 9 | "position": 1, 10 | "title": "Jenny Was a Friend of Mine", 11 | "length": "4:05" 12 | }, 13 | { "id": "1-2", "position": 2, "title": "Mr. Brightside", "length": "3:43" }, 14 | { 15 | "id": "1-3", 16 | "position": 3, 17 | "title": "Smile Like You Mean It", 18 | "length": "3:55" 19 | }, 20 | { 21 | "id": "1-4", 22 | "position": 4, 23 | "title": "Somebody Told Me", 24 | "length": "3:17" 25 | }, 26 | { 27 | "id": "1-5", 28 | "position": 5, 29 | "title": "All These Things That I've Done", 30 | "length": "5:02" 31 | }, 32 | { 33 | "id": "1-6", 34 | "position": 6, 35 | "title": "Andy, You're a Star", 36 | "length": "3:14" 37 | }, 38 | { "id": "1-7", "position": 7, "title": "On Top", "length": "4:19" }, 39 | { 40 | "id": "1-8", 41 | "position": 8, 42 | "title": "Change Your Mind", 43 | "length": "3:11" 44 | }, 45 | { 46 | "id": "1-9", 47 | "position": 9, 48 | "title": "Believe Me Natalie", 49 | "length": "5:07" 50 | }, 51 | { 52 | "id": "1-10", 53 | "position": 10, 54 | "title": "Midnight Show", 55 | "length": "4:03" 56 | }, 57 | { 58 | "id": "1-11", 59 | "position": 11, 60 | "title": "Everything Will Be Alright", 61 | "length": "5:45" 62 | } 63 | ] 64 | } 65 | -------------------------------------------------------------------------------- /src/data/album-2.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "2", 3 | "name": "First Impressions of Earth", 4 | "artist": "The Strokes", 5 | "img": "https://www.theaudiodb.com/images/media/album/thumb/rqsryv1477576489.jpg", 6 | "tracks": [ 7 | { 8 | "id": "2-1", 9 | "position": 1, 10 | "title": "You Only Live Once", 11 | "length": "3:09" 12 | }, 13 | { "id": "2-2", "position": 2, "title": "Juicebox", "length": "3:18" }, 14 | { 15 | "id": "2-3", 16 | "position": 3, 17 | "title": "Heart in a Cage", 18 | "length": "3:27" 19 | }, 20 | { "id": "2-4", "position": 4, "title": "Razorblade", "length": "3:29" }, 21 | { 22 | "id": "2-5", 23 | "position": 5, 24 | "title": "On the Other Side", 25 | "length": "4:39" 26 | }, 27 | { 28 | "id": "2-6", 29 | "position": 6, 30 | "title": "Vision of Division", 31 | "length": "4:20" 32 | }, 33 | { 34 | "id": "2-7", 35 | "position": 7, 36 | "title": "Ask Me Anything", 37 | "length": "3:13" 38 | }, 39 | { 40 | "id": "2-8", 41 | "position": 8, 42 | "title": "Electricityscape", 43 | "length": "3:33" 44 | }, 45 | { "id": "2-9", "position": 9, "title": "Killing Lies", "length": "3:50" }, 46 | { 47 | "id": "2-10", 48 | "position": 10, 49 | "title": "Fear of Sleep", 50 | "length": "4:00" 51 | }, 52 | { "id": "2-11", "position": 11, "title": "15 Minutes", "length": "4:34" }, 53 | { 54 | "id": "2-12", 55 | "position": 12, 56 | "title": "Ize of the World", 57 | "length": "4:29" 58 | }, 59 | { "id": "2-13", "position": 13, "title": "Evening Sun", "length": "3:06" }, 60 | { "id": "2-14", "position": 14, "title": "Red Light", "length": "3:12" } 61 | ] 62 | } 63 | -------------------------------------------------------------------------------- /src/data/album-3.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "3", 3 | "name": "Inside In/Inside Out", 4 | "artist": "The Kooks", 5 | "img": "https://www.theaudiodb.com/images/media/album/thumb/inside-ininside-out-4e5608971ceb2.jpeg", 6 | "tracks": [ 7 | { "id": "3-1", "position": 1, "title": "Seaside", "length": "1:39" }, 8 | { "id": "3-2", "position": 2, "title": "See the World", "length": "2:38" }, 9 | { "id": "3-3", "position": 3, "title": "Sofa Song", "length": "2:13" }, 10 | { "id": "3-4", "position": 4, "title": "Eddie’s Gun", "length": "2:13" }, 11 | { "id": "3-5", "position": 5, "title": "Ooh La", "length": "3:29" }, 12 | { 13 | "id": "3-6", 14 | "position": 6, 15 | "title": "You Don’t Love Me", 16 | "length": "2:35" 17 | }, 18 | { 19 | "id": "3-7", 20 | "position": 7, 21 | "title": "She Moves in Her Own Way", 22 | "length": "2:49" 23 | }, 24 | { "id": "3-8", "position": 8, "title": "Matchbox", "length": "3:10" }, 25 | { "id": "3-9", "position": 9, "title": "Naïve", "length": "3:24" }, 26 | { "id": "3-10", "position": 10, "title": "I Want You", "length": "3:27" }, 27 | { "id": "3-11", "position": 11, "title": "If Only", "length": "2:01" }, 28 | { 29 | "id": "3-12", 30 | "position": 12, 31 | "title": "Jackie Big Tees", 32 | "length": "2:33" 33 | }, 34 | { "id": "3-13", "position": 13, "title": "Time Awaits", "length": "5:09" }, 35 | { "id": "3-14", "position": 14, "title": "Got No Love", "length": "3:38" } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /src/data/album-4.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "4", 3 | "name": "Favourite Worst Nightmare", 4 | "artist": "Arctic Monkeys", 5 | "img": "https://www.theaudiodb.com/images/media/album/thumb/favourite-worst-nightmare-4e612d12273ea.jpg", 6 | "tracks": [ 7 | { "id": "4-1", "position": 1, "title": "Brianstorm", "length": "2:50" }, 8 | { "id": "4-2", "position": 2, "title": "Teddy Picker", "length": "2:43" }, 9 | { 10 | "id": "4-3", 11 | "position": 3, 12 | "title": "D Is for Dangerous", 13 | "length": "2:16" 14 | }, 15 | { "id": "4-4", "position": 4, "title": "Balaclava", "length": "2:49" }, 16 | { 17 | "id": "4-5", 18 | "position": 5, 19 | "title": "Fluorescent Adolescent", 20 | "length": "2:58" 21 | }, 22 | { 23 | "id": "4-6", 24 | "position": 6, 25 | "title": "Only Ones Who Know", 26 | "length": "3:03" 27 | }, 28 | { "id": "4-7", "position": 7, "title": "Do Me a Favour", "length": "3:27" }, 29 | { 30 | "id": "4-8", 31 | "position": 8, 32 | "title": "This House Is a Circus", 33 | "length": "3:10" 34 | }, 35 | { 36 | "id": "4-9", 37 | "position": 9, 38 | "title": "If You Were There, Beware", 39 | "length": "4:34" 40 | }, 41 | { 42 | "id": "4-10", 43 | "position": 10, 44 | "title": "The Bad Thing", 45 | "length": "2:23" 46 | }, 47 | { 48 | "id": "4-11", 49 | "position": 11, 50 | "title": "Old Yellow Bricks", 51 | "length": "3:11" 52 | }, 53 | { "id": "4-12", "position": 12, "title": "505", "length": "4:14" } 54 | ] 55 | } 56 | -------------------------------------------------------------------------------- /src/data/album-5.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "5", 3 | "name": "Up All Night", 4 | "artist": "Razorlight", 5 | "img": "https://www.theaudiodb.com/images/media/album/thumb/uxputq1399632252.jpg", 6 | "tracks": [ 7 | { "id": "5-1", "position": 1, "title": "Leave Me Alone", "length": "3:50" }, 8 | { 9 | "id": "5-2", 10 | "position": 2, 11 | "title": "Rock n Roll Lies", 12 | "length": "3:08" 13 | }, 14 | { "id": "5-3", "position": 3, "title": "Vice", "length": "3:14" }, 15 | { "id": "5-4", "position": 4, "title": "Up All Night", "length": "4:03" }, 16 | { 17 | "id": "5-5", 18 | "position": 5, 19 | "title": "Which Way Is Out", 20 | "length": "3:19" 21 | }, 22 | { "id": "5-6", "position": 6, "title": "Rip It Up", "length": "2:25" }, 23 | { 24 | "id": "5-7", 25 | "position": 7, 26 | "title": "Don’t Go Back to Dalston", 27 | "length": "2:59" 28 | }, 29 | { "id": "5-8", "position": 8, "title": "Golden Touch", "length": "3:25" }, 30 | { 31 | "id": "5-9", 32 | "position": 9, 33 | "title": "Stumble and Fall", 34 | "length": "3:02" 35 | }, 36 | { 37 | "id": "5-10", 38 | "position": 10, 39 | "title": "Get It and Go", 40 | "length": "3:23" 41 | }, 42 | { "id": "5-11", "position": 11, "title": "In the City", "length": "4:51" }, 43 | { "id": "5-12", "position": 12, "title": "To the Sea", "length": "5:31" }, 44 | { 45 | "id": "5-13", 46 | "position": 13, 47 | "title": "Fall, Fall, Fall", 48 | "length": "2:43" 49 | } 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /src/data/album-6.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "6", 3 | "name": "Phrazes for the Young", 4 | "artist": "Julian Casablancas", 5 | "img": "https://www.theaudiodb.com/images/media/album/thumb/phrazes-for-the-young-52f7faa979a19.jpg", 6 | "tracks": [ 7 | { 8 | "id": "6-1", 9 | "position": 1, 10 | "title": "Out of the Blue", 11 | "length": "4:43" 12 | }, 13 | { 14 | "id": "6-2", 15 | "position": 2, 16 | "title": "Left & Right in the Dark", 17 | "length": "4:59" 18 | }, 19 | { "id": "6-3", "position": 3, "title": "11th Dimension", "length": "4:06" }, 20 | { 21 | "id": "6-4", 22 | "position": 4, 23 | "title": "4 Chords of the Apocalypse", 24 | "length": "5:02" 25 | }, 26 | { "id": "6-5", "position": 5, "title": "Ludlow St.", "length": "5:45" }, 27 | { 28 | "id": "6-6", 29 | "position": 6, 30 | "title": "River of Brakelights", 31 | "length": "5:13" 32 | }, 33 | { "id": "6-7", "position": 7, "title": "Glass", "length": "5:25" }, 34 | { "id": "6-8", "position": 8, "title": "Tourist", "length": "5:03" } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /src/data/album-7.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "7", 3 | "name": "Franz Ferdinand", 4 | "artist": "Franz Ferdinand", 5 | "img": "https://www.theaudiodb.com/images/media/album/thumb/franz-ferdinand-4dd465083a9a8.jpg", 6 | "tracks": [ 7 | { "id": "7-1", "position": 1, "title": "Jacqueline", "length": "3:49" }, 8 | { 9 | "id": "7-2", 10 | "position": 2, 11 | "title": "Tell Her Tonight", 12 | "length": "2:18" 13 | }, 14 | { "id": "7-3", "position": 3, "title": "Take Me Out", "length": "3:57" }, 15 | { 16 | "id": "7-4", 17 | "position": 4, 18 | "title": "The Dark of the Matinée", 19 | "length": "4:03" 20 | }, 21 | { "id": "7-5", "position": 5, "title": "Auf Achse", "length": "4:20" }, 22 | { 23 | "id": "7-6", 24 | "position": 6, 25 | "title": "Cheating on You", 26 | "length": "2:37" 27 | }, 28 | { "id": "7-7", "position": 7, "title": "This Fire", "length": "4:15" }, 29 | { 30 | "id": "7-8", 31 | "position": 8, 32 | "title": "Darts of Pleasure", 33 | "length": "3:00" 34 | }, 35 | { "id": "7-9", "position": 9, "title": "Michael", "length": "3:21" }, 36 | { "id": "7-10", "position": 10, "title": "Come On Home", "length": "3:46" }, 37 | { "id": "7-11", "position": 11, "title": "40′", "length": "3:23" } 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /src/data/album-8.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "8", 3 | "name": "Get Born", 4 | "artist": "Jet", 5 | "img": "https://www.theaudiodb.com/images/media/album/thumb/get-born-4ddedb9009071.jpg", 6 | "tracks": [ 7 | { "id": "8-1", "position": 1, "title": "Last Chance", "length": "1:52" }, 8 | { 9 | "id": "8-2", 10 | "position": 2, 11 | "title": "Are You Gonna Be My Girl", 12 | "length": "3:34" 13 | }, 14 | { "id": "8-3", "position": 3, "title": "Rollover D.J.", "length": "3:17" }, 15 | { 16 | "id": "8-4", 17 | "position": 4, 18 | "title": "Look What You’ve Done", 19 | "length": "3:51" 20 | }, 21 | { 22 | "id": "8-5", 23 | "position": 5, 24 | "title": "Get What You Need", 25 | "length": "4:08" 26 | }, 27 | { "id": "8-6", "position": 6, "title": "Move On", "length": "4:21" }, 28 | { "id": "8-7", "position": 7, "title": "Radio Song", "length": "4:32" }, 29 | { 30 | "id": "8-8", 31 | "position": 8, 32 | "title": "Get Me Outta Here", 33 | "length": "2:56" 34 | }, 35 | { 36 | "id": "8-9", 37 | "position": 9, 38 | "title": "Cold Hard Peach", 39 | "length": "4:03" 40 | }, 41 | { 42 | "id": "8-10", 43 | "position": 10, 44 | "title": "Come Around Again", 45 | "length": "4:30" 46 | }, 47 | { 48 | "id": "8-11", 49 | "position": 11, 50 | "title": "Take It or Leave It", 51 | "length": "2:23" 52 | }, 53 | { "id": "8-12", "position": 12, "title": "Lazy Gun", "length": "4:42" }, 54 | { "id": "8-13", "position": 13, "title": "Timothy", "length": "4:30" } 55 | ] 56 | } 57 | -------------------------------------------------------------------------------- /src/data/albums.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "1", 4 | "name": "Hot Fuss", 5 | "artist": "The Killers", 6 | "img": "https://www.theaudiodb.com/images/media/album/thumb/hot-fuss-limited-edition-7-inch-box-set-4ddc38e3e1d71.jpg" 7 | }, 8 | { 9 | "id": "2", 10 | "name": "First Impressions of Earth", 11 | "artist": "The Strokes", 12 | "img": "https://www.theaudiodb.com/images/media/album/thumb/rqsryv1477576489.jpg" 13 | }, 14 | { 15 | "id": "3", 16 | "name": "Inside In/Inside Out", 17 | "artist": "The Kooks", 18 | "img": "https://www.theaudiodb.com/images/media/album/thumb/inside-ininside-out-4e5608971ceb2.jpeg" 19 | }, 20 | { 21 | "id": "4", 22 | "name": "Favourite Worst Nightmare", 23 | "artist": "Arctic Monkeys", 24 | "img": "https://www.theaudiodb.com/images/media/album/thumb/favourite-worst-nightmare-4e612d12273ea.jpg" 25 | }, 26 | { 27 | "id": "5", 28 | "name": "Up All Night", 29 | "artist": "Razorlight", 30 | "img": "https://www.theaudiodb.com/images/media/album/thumb/uxputq1399632252.jpg" 31 | }, 32 | { 33 | "id": "6", 34 | "name": "Phrazes for the Young", 35 | "artist": "Julian Casablancas", 36 | "img": "https://www.theaudiodb.com/images/media/album/thumb/phrazes-for-the-young-52f7faa979a19.jpg" 37 | }, 38 | { 39 | "id": "7", 40 | "name": "Franz Ferdinand", 41 | "artist": "Franz Ferdinand", 42 | "img": "https://www.theaudiodb.com/images/media/album/thumb/franz-ferdinand-4dd465083a9a8.jpg" 43 | }, 44 | { 45 | "id": "8", 46 | "name": "Get Botn", 47 | "artist": "Jet", 48 | "img": "https://www.theaudiodb.com/images/media/album/thumb/get-born-4ddedb9009071.jpg" 49 | } 50 | ] 51 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .vinyl-animation-in { 6 | animation: appear 600ms ease-in-out 300ms forwards; 7 | } 8 | .vinyl-animation-in-spinning { 9 | animation: appear 600ms ease-in-out 300ms forwards, spin 5s linear infinite; 10 | } 11 | .vinyl-animation-out { 12 | animation: disappear 300ms ease-in-out forwards; 13 | } 14 | 15 | @keyframes spin { 16 | from { 17 | transform: rotate(0deg); 18 | } 19 | to { 20 | transform: rotate(360deg); 21 | } 22 | } 23 | @keyframes appear { 24 | 0% { 25 | left: 0; 26 | opacity: 0; 27 | } 28 | 1% { 29 | opacity: 1; 30 | } 31 | 100% { 32 | left: 24%; 33 | opacity: 1; 34 | } 35 | } 36 | 37 | @keyframes disappear { 38 | 0% { 39 | left: 24%; 40 | opacity: 1; 41 | } 42 | 99% { 43 | opacity: 1; 44 | } 45 | 100% { 46 | left: 0; 47 | opacity: 0; 48 | } 49 | } 50 | 51 | a.transitioning .c-card--album, 52 | .c-record--album { 53 | view-transition-name: album-expand; 54 | } 55 | 56 | ::view-transition-old(album-expand):not(:only-child), 57 | ::view-transition-new(album-expand):not(:only-child) { 58 | animation: none; 59 | mix-blend-mode: normal; 60 | } 61 | 62 | a.transitioning .c-card--vinyl, 63 | .c-record--vinyl { 64 | view-transition-name: vinyl-expand; 65 | } 66 | 67 | ::view-transition-old(vinyl-expand):not(:only-child), 68 | ::view-transition-new(vinyl-expand):not(:only-child) { 69 | animation: none; 70 | mix-blend-mode: normal; 71 | } 72 | 73 | .c-player { 74 | view-transition-name: "player"; 75 | } 76 | 77 | ::view-transition-old(player) { 78 | display: none; 79 | } 80 | 81 | ::view-transition-new(player) { 82 | animation: none; 83 | } 84 | -------------------------------------------------------------------------------- /src/main.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import App from "./App"; 4 | import "./index.css"; 5 | 6 | ReactDOM.createRoot(document.getElementById("root")).render( 7 | 8 | 9 | 10 | ); 11 | -------------------------------------------------------------------------------- /src/routes/Album.tsx: -------------------------------------------------------------------------------- 1 | import { LoaderFunctionArgs, useLoaderData } from "react-router-dom"; 2 | import Record from "../components/Record"; 3 | import TrackList from "../components/TrackList"; 4 | import PlayButton from "../components/PlayButton"; 5 | import { type Track } from "./Layout"; 6 | 7 | type AlbumWithTracks = { 8 | id: string; 9 | name: string; 10 | artist: string; 11 | img: string; 12 | tracks: Track[]; 13 | }; 14 | 15 | export async function loader({ params }: LoaderFunctionArgs) { 16 | // TODO: Fix synchronous navigations 17 | await new Promise((r) => setTimeout(r, 1)); 18 | const { default: album }: { default: AlbumWithTracks } = await import( 19 | `../data/album-${params.id}.json` 20 | ); 21 | return album; 22 | } 23 | 24 | export function Component() { 25 | let album = useLoaderData() as Awaited>; 26 | 27 | return ( 28 |
29 |
30 | 36 |
37 |

38 | {album.name} 39 |

40 |

{album.artist}

41 |

42 | {album.strGenre} — {album.intYearReleased} 43 |

44 |
45 | 51 | 52 | 70 |
71 |
72 |
73 | 74 |
75 | 82 |
83 |
84 | ); 85 | } 86 | -------------------------------------------------------------------------------- /src/routes/Home.tsx: -------------------------------------------------------------------------------- 1 | import { useLoaderData } from "react-router-dom"; 2 | import { Card } from "../components/Card"; 3 | 4 | export async function loader() { 5 | // TODO: Fix synchronous navigations 6 | await new Promise((r) => setTimeout(r, 1)); 7 | const { default: albums } = await import("../data/albums.json"); 8 | return albums; 9 | } 10 | 11 | export function Component() { 12 | let albums = useLoaderData() as Awaited>; 13 | 14 | return ( 15 |
16 |
17 |

18 | Recently Played 19 |

20 | 21 |
22 | {albums.map((album) => ( 23 | 30 | ))} 31 |
32 |
33 |
34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /src/routes/Layout.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Outlet } from "react-router-dom"; 3 | import Header from "../components/Header"; 4 | import Footer from "../components/Footer"; 5 | import Player from "../components/Player"; 6 | 7 | export type Track = { 8 | id: string; 9 | title: string; 10 | position: number; 11 | length: string; 12 | }; 13 | 14 | export type PlayerTrack = Track & { 15 | albumId: string; 16 | artist: string; 17 | imageUrl: string; 18 | }; 19 | 20 | export const PlayerContext = React.createContext<{ 21 | isPlaying: boolean; 22 | setIsPlaying: (v: boolean) => void; 23 | currentTrack: PlayerTrack | null; 24 | setCurrentTrack: (v: PlayerTrack) => void; 25 | }>({ 26 | isPlaying: false, 27 | setIsPlaying: () => {}, 28 | currentTrack: null, 29 | setCurrentTrack: () => {}, 30 | }); 31 | 32 | export default function Layout() { 33 | let [isPlaying, setIsPlaying] = React.useState(false); 34 | let [currentTrack, setCurrentTrack] = React.useState( 35 | null 36 | ); 37 | 38 | console.log(isPlaying, currentTrack); 39 | 40 | return ( 41 | 49 |
50 | 51 |