├── bun.lockb ├── app ├── favicon.ico ├── albums │ └── [slug] │ │ └── page.tsx ├── playlists │ └── [slug] │ │ └── page.tsx ├── globals.css ├── page.tsx ├── layout.tsx ├── search │ └── page.tsx ├── library │ └── page.tsx ├── liked │ └── page.tsx ├── actions.ts └── not-found.tsx ├── postcss.config.mjs ├── public ├── vercel.svg ├── window.svg ├── file.svg ├── globe.svg └── next.svg ├── next.config.ts ├── lib ├── cosmic.ts └── PlayerContext.tsx ├── eslint.config.mjs ├── .gitignore ├── tsconfig.json ├── package.json ├── types └── index.ts ├── components ├── AlbumCard.tsx ├── Footer.tsx ├── MusicLibrary.tsx ├── Loading.tsx ├── TrackList.tsx ├── AlbumDetail.tsx ├── PlaylistClient.tsx ├── MusicPlayer.tsx └── Navigation.tsx ├── README.md ├── scripts └── seed-cosmic.ts └── article.md /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicjs/cosmic-spotify-clone/main/bun.lockb -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicjs/cosmic-spotify-clone/main/app/favicon.ico -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: ["@tailwindcss/postcss"], 3 | }; 4 | 5 | export default config; 6 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | /* config options here */ 5 | }; 6 | 7 | export default nextConfig; 8 | -------------------------------------------------------------------------------- /lib/cosmic.ts: -------------------------------------------------------------------------------- 1 | import { createBucketClient } from "@cosmicjs/sdk"; 2 | 3 | const cosmic = createBucketClient({ 4 | bucketSlug: process.env.COSMIC_BUCKET_SLUG as string, 5 | readKey: process.env.COSMIC_READ_KEY as string, 6 | }); 7 | 8 | export default cosmic; 9 | -------------------------------------------------------------------------------- /app/albums/[slug]/page.tsx: -------------------------------------------------------------------------------- 1 | import AlbumDetail from "@/components/AlbumDetail"; 2 | 3 | interface AlbumPageProps { 4 | params: Promise<{ 5 | slug: string; 6 | }>; 7 | } 8 | 9 | export default async function AlbumPage({ params }: AlbumPageProps) { 10 | const { slug } = await params; 11 | return ; 12 | } 13 | -------------------------------------------------------------------------------- /app/playlists/[slug]/page.tsx: -------------------------------------------------------------------------------- 1 | import PlaylistClient from "@/components/PlaylistClient"; 2 | 3 | interface PlaylistPageProps { 4 | params: Promise<{ 5 | slug: string; 6 | }>; 7 | } 8 | 9 | export default async function PlaylistPage({ params }: PlaylistPageProps) { 10 | const { slug } = await params; 11 | return ; 12 | } 13 | -------------------------------------------------------------------------------- /public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { dirname } from "path"; 2 | import { fileURLToPath } from "url"; 3 | import { FlatCompat } from "@eslint/eslintrc"; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = dirname(__filename); 7 | 8 | const compat = new FlatCompat({ 9 | baseDirectory: __dirname, 10 | }); 11 | 12 | const eslintConfig = [ 13 | ...compat.extends("next/core-web-vitals", "next/typescript"), 14 | ]; 15 | 16 | export default eslintConfig; 17 | -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | 3 | :root { 4 | --background: #ffffff; 5 | --foreground: #171717; 6 | } 7 | 8 | @theme inline { 9 | --color-background: var(--background); 10 | --color-foreground: var(--foreground); 11 | --font-sans: var(--font-geist-sans); 12 | --font-mono: var(--font-geist-mono); 13 | } 14 | 15 | @media (prefers-color-scheme: dark) { 16 | :root { 17 | --background: #0a0a0a; 18 | --foreground: #ededed; 19 | } 20 | } 21 | 22 | body { 23 | background: var(--background); 24 | color: var(--foreground); 25 | font-family: Arial, Helvetica, sans-serif; 26 | } 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env* 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | import MusicLibrary from "@/components/MusicLibrary"; 2 | 3 | export default function Home() { 4 | return ( 5 |
6 |

Welcome to Spotify Clone

7 |

8 | Powered by{" "} 9 | 15 | Cosmic 16 | 17 |

18 | 19 |
20 |

Browse Albums

21 | 22 |
23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spotify-clone", 3 | "version": "0.1.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "dev": "next dev --turbopack", 8 | "build": "next build", 9 | "start": "next start", 10 | "lint": "next lint" 11 | }, 12 | "dependencies": { 13 | "@cosmicjs/sdk": "^1.4.0", 14 | "next": "15.2.3", 15 | "react": "^19.0.0", 16 | "react-audio-player": "^0.17.0", 17 | "react-dom": "^19.0.0" 18 | }, 19 | "devDependencies": { 20 | "@eslint/eslintrc": "^3", 21 | "@tailwindcss/postcss": "^4", 22 | "@types/node": "^20", 23 | "@types/react": "^19", 24 | "@types/react-dom": "^19", 25 | "eslint": "^9", 26 | "eslint-config-next": "15.2.3", 27 | "tailwindcss": "^4", 28 | "ts-node": "^10.9.2", 29 | "typescript": "^5" 30 | } 31 | } -------------------------------------------------------------------------------- /types/index.ts: -------------------------------------------------------------------------------- 1 | export interface Track { 2 | id: string; 3 | title: string; 4 | metadata: { 5 | audio: { 6 | url: string; 7 | }; 8 | duration: number; 9 | album: { 10 | metadata: { 11 | cover: { 12 | imgix_url: string; 13 | }; 14 | artist: { 15 | title: string; 16 | }; 17 | }; 18 | }; 19 | }; 20 | } 21 | 22 | export interface Album { 23 | id: string; 24 | slug: string; 25 | title: string; 26 | metadata: { 27 | cover: { 28 | imgix_url: string; 29 | }; 30 | artist: { 31 | title: string; 32 | }; 33 | }; 34 | } 35 | 36 | export interface Playlist { 37 | id: string; 38 | slug: string; 39 | title: string; 40 | metadata: { 41 | description: string; 42 | cover: { 43 | imgix_url: string; 44 | }; 45 | tracks: Track[]; 46 | }; 47 | } 48 | -------------------------------------------------------------------------------- /public/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/AlbumCard.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { Album } from "@/types"; 3 | 4 | interface AlbumCardProps { 5 | album: Album; 6 | } 7 | 8 | export default function AlbumCard({ album }: AlbumCardProps) { 9 | return ( 10 | 11 |
12 | {album.title} 19 |

20 | {album.title} 21 |

22 |

23 | {album.metadata.artist?.title || "Unknown artist"} 24 |

25 |
26 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | import "./globals.css"; 4 | import { PlayerProvider } from "@/lib/PlayerContext"; 5 | import Navigation from "@/components/Navigation"; 6 | import Footer from "@/components/Footer"; 7 | 8 | const inter = Inter({ subsets: ["latin"] }); 9 | 10 | export const metadata: Metadata = { 11 | title: "Spotify Clone - Built with Cosmic CMS", 12 | description: "A demo app showing what's possible with Cosmic CMS", 13 | openGraph: { 14 | images: [ 15 | { 16 | url: "https://imgix.cosmicjs.com/2797d440-05d5-11f0-993b-3bd041905fff-quantum.jpg", 17 | width: 1200, 18 | height: 630, 19 | alt: "Spotify Clone Preview", 20 | }, 21 | ], 22 | }, 23 | }; 24 | 25 | export default function RootLayout({ 26 | children, 27 | }: { 28 | children: React.ReactNode; 29 | }) { 30 | return ( 31 | 32 | 33 | 34 | 35 |
36 |
{children}
37 |
38 |