setIsMobileMenuOpen(false)}
277 | />
278 | )}
279 | >
280 | );
281 | }
282 |
--------------------------------------------------------------------------------
/scripts/seed-cosmic.ts:
--------------------------------------------------------------------------------
1 | import { createBucketClient } from "@cosmicjs/sdk";
2 |
3 | const BUCKET_SLUG = process.env.COSMIC_BUCKET_SLUG;
4 | const WRITE_KEY = process.env.COSMIC_WRITE_KEY;
5 | const READ_KEY = process.env.COSMIC_READ_KEY;
6 |
7 | if (!BUCKET_SLUG || !WRITE_KEY || !READ_KEY) {
8 | throw new Error("Missing required environment variables");
9 | }
10 |
11 | const cosmic = createBucketClient({
12 | bucketSlug: BUCKET_SLUG,
13 | writeKey: WRITE_KEY,
14 | readKey: READ_KEY,
15 | });
16 |
17 | async function uploadImageFromUrl(url: string, filename: string) {
18 | try {
19 | console.log(`Fetching image from ${url}...`);
20 | const response = await fetch(url);
21 |
22 | if (!response.ok) {
23 | throw new Error(`HTTP error! status: ${response.status}`);
24 | }
25 |
26 | console.log("Converting to blob...");
27 | const blob = await response.blob();
28 | console.log(`Blob type: ${blob.type}, size: ${blob.size} bytes`);
29 |
30 | console.log("Converting to buffer...");
31 | const buffer = await blob.arrayBuffer();
32 | console.log(`Buffer length: ${buffer.byteLength} bytes`);
33 |
34 | console.log("Uploading to Cosmic...");
35 | const { media } = await cosmic.media.insertOne({
36 | media: {
37 | originalname: filename,
38 | buffer: Buffer.from(buffer),
39 | },
40 | });
41 |
42 | console.log(`Successfully uploaded ${filename} to Cosmic`);
43 | return media;
44 | } catch (error) {
45 | console.error(`Error uploading image from ${url}:`);
46 | console.error("Full error:", error);
47 | if (error instanceof Error) {
48 | console.error("Error message:", error.message);
49 | console.error("Error stack:", error.stack);
50 | }
51 | throw error;
52 | }
53 | }
54 |
55 | async function uploadAudioFromUrl(url: string, filename: string) {
56 | try {
57 | console.log(`Fetching audio from ${url}...`);
58 | const response = await fetch(url);
59 |
60 | if (!response.ok) {
61 | throw new Error(`HTTP error! status: ${response.status}`);
62 | }
63 |
64 | console.log("Converting to blob...");
65 | const blob = await response.blob();
66 | console.log(`Blob type: ${blob.type}, size: ${blob.size} bytes`);
67 |
68 | console.log("Converting to buffer...");
69 | const buffer = await blob.arrayBuffer();
70 | console.log(`Buffer length: ${buffer.byteLength} bytes`);
71 |
72 | console.log("Uploading to Cosmic...");
73 | const { media } = await cosmic.media.insertOne({
74 | media: {
75 | originalname: filename,
76 | buffer: Buffer.from(buffer),
77 | },
78 | });
79 |
80 | console.log(`Successfully uploaded ${filename} to Cosmic`);
81 | return media;
82 | } catch (error) {
83 | console.error(`Error uploading audio from ${url}:`);
84 | console.error("Full error:", error);
85 | if (error instanceof Error) {
86 | console.error("Error message:", error.message);
87 | console.error("Error stack:", error.stack);
88 | }
89 | throw error;
90 | }
91 | }
92 |
93 | async function seedObjectTypes() {
94 | try {
95 | // Create Artists object type
96 | await cosmic.objectTypes.insertOne({
97 | title: "Artists",
98 | slug: "artists",
99 | singular: "Artist",
100 | emoji: "👤",
101 | metafields: [
102 | {
103 | title: "Name",
104 | key: "name",
105 | type: "text",
106 | required: true,
107 | },
108 | {
109 | title: "Image",
110 | key: "image",
111 | type: "file",
112 | required: true,
113 | media_validation_type: "image",
114 | },
115 | {
116 | title: "Bio",
117 | key: "bio",
118 | type: "textarea",
119 | required: true,
120 | },
121 | ],
122 | });
123 |
124 | // Create Albums object type
125 | await cosmic.objectTypes.insertOne({
126 | title: "Albums",
127 | slug: "albums",
128 | singular: "Album",
129 | emoji: "💿",
130 | metafields: [
131 | {
132 | title: "Title",
133 | key: "title",
134 | type: "text",
135 | required: true,
136 | },
137 | {
138 | title: "Cover",
139 | key: "cover",
140 | type: "file",
141 | required: true,
142 | media_validation_type: "image",
143 | },
144 | {
145 | title: "Release Date",
146 | key: "release_date",
147 | type: "date",
148 | required: true,
149 | },
150 | {
151 | title: "Artist",
152 | key: "artist",
153 | type: "object",
154 | object_type: "artists",
155 | required: true,
156 | },
157 | ],
158 | });
159 |
160 | // Create Tracks object type
161 | await cosmic.objectTypes.insertOne({
162 | title: "Tracks",
163 | slug: "tracks",
164 | singular: "Track",
165 | emoji: "🎵",
166 | metafields: [
167 | {
168 | title: "Title",
169 | key: "title",
170 | type: "text",
171 | required: true,
172 | },
173 | {
174 | title: "Audio File",
175 | key: "audio",
176 | type: "file",
177 | required: true,
178 | media_validation_type: "audio",
179 | },
180 | {
181 | title: "Duration",
182 | key: "duration",
183 | type: "number",
184 | required: true,
185 | },
186 | {
187 | title: "Album",
188 | key: "album",
189 | type: "object",
190 | object_type: "albums",
191 | required: true,
192 | },
193 | ],
194 | });
195 |
196 | // Create Playlists object type
197 | await cosmic.objectTypes.insertOne({
198 | title: "Playlists",
199 | slug: "playlists",
200 | singular: "Playlist",
201 | emoji: "📀",
202 | metafields: [
203 | {
204 | title: "Title",
205 | key: "title",
206 | type: "text",
207 | required: true,
208 | },
209 | {
210 | title: "Description",
211 | key: "description",
212 | type: "textarea",
213 | required: true,
214 | },
215 | {
216 | title: "Cover",
217 | key: "cover",
218 | type: "file",
219 | required: true,
220 | media_validation_type: "image",
221 | },
222 | {
223 | title: "Tracks",
224 | key: "tracks",
225 | type: "objects",
226 | object_type: "tracks",
227 | required: false,
228 | },
229 | ],
230 | });
231 |
232 | console.log("Successfully created all object types!");
233 | } catch (error) {
234 | console.error("Error creating object types:", error);
235 | process.exit(1);
236 | }
237 | }
238 |
239 | async function seedContent() {
240 | try {
241 | // Upload artist images
242 | const lunaImage = await uploadImageFromUrl(
243 | "https://images.unsplash.com/photo-1494354145959-25cb82edf23d?w=400&h=400&fit=crop",
244 | "luna-moon.jpg"
245 | );
246 | const novaImage = await uploadImageFromUrl(
247 | "https://images.unsplash.com/photo-1516223725307-6f76b9ec8742?w=400&h=400&fit=crop",
248 | "nova-star.jpg"
249 | );
250 |
251 | // Create sample artists
252 | const { object: artist1 } = await cosmic.objects.insertOne({
253 | title: "Luna Moon",
254 | slug: "luna-moon",
255 | type: "artists",
256 | thumbnail: lunaImage.name,
257 | metadata: {
258 | name: "Luna Moon",
259 | bio: "Luna Moon is a cosmic pop sensation known for her ethereal vocals and space-themed music. Her unique blend of electronic and acoustic elements has created a new genre called 'astro-pop'.",
260 | image: lunaImage.name,
261 | },
262 | });
263 |
264 | const { object: artist2 } = await cosmic.objects.insertOne({
265 | title: "Nova Star",
266 | slug: "nova-star",
267 | type: "artists",
268 | thumbnail: novaImage.name,
269 | metadata: {
270 | name: "Nova Star",
271 | bio: "Nova Star is an indie rock phenomenon who writes songs about quantum physics and parallel universes. His experimental sound has earned him the nickname 'The Einstein of Rock'.",
272 | image: novaImage.name,
273 | },
274 | });
275 |
276 | // Upload album covers
277 | const starlightCover = await uploadImageFromUrl(
278 | "https://images.unsplash.com/photo-1419242902214-272b3f66ee7a?w=400&h=400&fit=crop",
279 | "starlight.jpg"
280 | );
281 | const quantumCover = await uploadImageFromUrl(
282 | "https://images.unsplash.com/photo-1557264337-e8a93017fe92?w=400&h=400&fit=crop",
283 | "quantum.jpg"
284 | );
285 |
286 | // Create sample albums
287 | const { object: album1 } = await cosmic.objects.insertOne({
288 | title: "Starlight Symphony",
289 | slug: "starlight-symphony",
290 | type: "albums",
291 | thumbnail: starlightCover.name,
292 | metadata: {
293 | title: "Starlight Symphony",
294 | release_date: "2023-06-15",
295 | artist: artist1.id,
296 | cover: starlightCover.name,
297 | },
298 | });
299 |
300 | const { object: album2 } = await cosmic.objects.insertOne({
301 | title: "Quantum Dreams",
302 | slug: "quantum-dreams",
303 | type: "albums",
304 | thumbnail: quantumCover.name,
305 | metadata: {
306 | title: "Quantum Dreams",
307 | release_date: "2023-08-22",
308 | artist: artist2.id,
309 | cover: quantumCover.name,
310 | },
311 | });
312 |
313 | // Create sample tracks
314 | const cosmicDanceAudio = await uploadAudioFromUrl(
315 | "https://cdn.cosmicjs.com/1474f620-05be-11f0-993b-3bd041905fff-relaxing-jazz-saxophone-music-saxophone-instruments-music-303093.mp3",
316 | "cosmic-dance.mp3"
317 | );
318 |
319 | const parallelWorldsAudio = await uploadAudioFromUrl(
320 | "https://cdn.cosmicjs.com/147e44f0-05be-11f0-993b-3bd041905fff-iced-coffee-jazz-309947.mp3",
321 | "parallel-worlds.mp3"
322 | );
323 |
324 | const { object: track1 } = await cosmic.objects.insertOne({
325 | title: "Cosmic Dance",
326 | slug: "cosmic-dance",
327 | type: "tracks",
328 | metadata: {
329 | title: "Cosmic Dance",
330 | duration: 245,
331 | album: album1.id,
332 | audio: cosmicDanceAudio.name,
333 | },
334 | });
335 |
336 | const { object: track2 } = await cosmic.objects.insertOne({
337 | title: "Parallel Worlds",
338 | slug: "parallel-worlds",
339 | type: "tracks",
340 | metadata: {
341 | title: "Parallel Worlds",
342 | duration: 312,
343 | album: album2.id,
344 | audio: parallelWorldsAudio.name,
345 | },
346 | });
347 |
348 | // Upload playlist cover
349 | const playlistCover = await uploadImageFromUrl(
350 | "https://images.unsplash.com/photo-1470225620780-dba8ba36b745?w=400&h=400&fit=crop",
351 | "cosmic-hits.jpg"
352 | );
353 |
354 | // Create sample playlist
355 | await cosmic.objects.insertOne({
356 | title: "Cosmic Hits 2023",
357 | slug: "cosmic-hits-2023",
358 | type: "playlists",
359 | thumbnail: playlistCover.name,
360 | metadata: {
361 | title: "Cosmic Hits 2023",
362 | description:
363 | "A stellar collection of the year's most out-of-this-world tracks",
364 | tracks: [track1.id, track2.id],
365 | cover: playlistCover.name,
366 | },
367 | });
368 |
369 | console.log("Successfully seeded all content!");
370 | } catch (error) {
371 | console.error("Error seeding content:", error);
372 | process.exit(1);
373 | }
374 | }
375 |
376 | // Run both seeding functions
377 | async function seed() {
378 | await seedObjectTypes();
379 | await seedContent();
380 | }
381 |
382 | seed();
383 |
--------------------------------------------------------------------------------
/article.md:
--------------------------------------------------------------------------------
1 | # How to Build a Spotify Clone with Next.js and Cosmic
2 |
3 | Streaming music platforms have revolutionized how we consume audio content. In this tutorial, we'll build a Spotify-like music streaming application using Next.js for the frontend and [Cosmic](https://www.cosmicjs.com) as our headless CMS to manage music tracks, artists, and playlists.
4 |
5 | [Live Demo](https://cosmic-spotify-clone.vercel.app)
6 |
7 | [GitHub Repository](https://github.com/cosmicjs/cosmic-spotify-clone)
8 |
9 | [Deploy with Vercel](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fcosmicjs%2Fcosmic-spotify-clone&env=COSMIC_BUCKET_SLUG,COSMIC_READ_KEY,COSMIC_WRITE_KEY&envDescription=Required%20API%20keys%20from%20Cosmic&envLink=https%3A%2F%2Fwww.cosmicjs.com%2Fdocs%2Fapi%2Fauthentication)
10 |
11 | [Spotify Clone Screenshot 2](https://imgix.cosmicjs.com/9d793800-05d7-11f0-993b-3bd041905fff-cosmic-spotify-2.png?w=2000&auto=format,compression)
12 |
13 | [Spotify Clone Screenshot 1](https://imgix.cosmicjs.com/9d6784c0-05d7-11f0-993b-3bd041905fff-cosmic-spotify-1.png?w=2000&auto=format,compression)
14 |
15 | ## What We'll Build
16 |
17 | Our Spotify clone will feature:
18 |
19 | - Music library with artists, albums, and tracks
20 | - Playlist creation and management
21 | - A music player with playback controls
22 | - Responsive design for all devices
23 |
24 | ## Prerequisites
25 |
26 | - Node.js 18.x or later (if using npm)
27 | - Bun runtime (if using bun, recommended)
28 | - A Cosmic account and bucket
29 | - Basic knowledge of React and TypeScript
30 |
31 | ## Setting Up Our Project
32 |
33 | First, let's create a new Next.js project with TypeScript:
34 |
35 | Using bun (recommended):
36 |
37 | ```bash
38 | bunx create-next-app spotify-clone --typescript
39 | cd spotify-clone
40 | ```
41 |
42 | Using npm:
43 |
44 | ```bash
45 | npx create-next-app spotify-clone --typescript
46 | cd spotify-clone
47 | ```
48 |
49 | Install the required dependencies:
50 |
51 | Using bun:
52 |
53 | ```bash
54 | bun add @cosmicjs/sdk react-audio-player
55 | bun add -d tailwindcss postcss autoprefixer @types/react-audio-player
56 | ```
57 |
58 | Using npm:
59 |
60 | ```bash
61 | npm install @cosmicjs/sdk react-audio-player
62 | npm install -D tailwindcss postcss autoprefixer @types/react-audio-player
63 | ```
64 |
65 | Initialize Tailwind CSS:
66 |
67 | Using bun:
68 |
69 | ```bash
70 | bunx tailwindcss init -p
71 | ```
72 |
73 | Using npm:
74 |
75 | ```bash
76 | npx tailwindcss init -p
77 | ```
78 |
79 | Update your `tailwind.config.js`:
80 |
81 | ```javascript
82 | /** @type {import('tailwindcss').Config} */
83 | module.exports = {
84 | content: [
85 | "./app/**/*.{js,ts,jsx,tsx,mdx}",
86 | "./components/**/*.{js,ts,jsx,tsx,mdx}",
87 | ],
88 | theme: {
89 | extend: {},
90 | },
91 | plugins: [],
92 | };
93 | ```
94 |
95 | ## Setting Up Environment Variables
96 |
97 | Create a `.env.local` file in your project root:
98 |
99 | ```env
100 | COSMIC_BUCKET_SLUG=your-bucket-slug
101 | COSMIC_READ_KEY=your-read-key
102 | COSMIC_WRITE_KEY=your-write-key
103 | ```
104 |
105 | ## Project Structure
106 |
107 | Let's organize our project with the following structure:
108 |
109 | ```
110 | spotify-clone/
111 | ├── app/
112 | │ ├── layout.tsx
113 | │ ├── page.tsx
114 | │ ├── albums/
115 | │ │ └── [slug]/
116 | │ │ └── page.tsx
117 | │ └── playlists/
118 | │ └── [slug]/
119 | │ └── page.tsx
120 | ├── components/
121 | │ ├── AlbumCard.tsx
122 | │ ├── AlbumDetail.tsx
123 | │ ├── MusicLibrary.tsx
124 | │ ├── MusicPlayer.tsx
125 | │ ├── Navigation.tsx
126 | │ ├── Footer.tsx
127 | │ └── TrackList.tsx
128 | ├── lib/
129 | │ ├── cosmic.ts
130 | │ └── PlayerContext.tsx
131 | ├── types/
132 | │ └── index.ts
133 | └── scripts/
134 | └── seed-cosmic.ts
135 | ```
136 |
137 | ## Setting Up TypeScript Types
138 |
139 | Create `types/index.ts` to define our data types:
140 |
141 | ```typescript
142 | export interface Track {
143 | id: string;
144 | title: string;
145 | metadata: {
146 | audio: {
147 | url: string;
148 | };
149 | duration: number;
150 | album: {
151 | metadata: {
152 | cover: {
153 | imgix_url: string;
154 | };
155 | artist: {
156 | title: string;
157 | };
158 | };
159 | };
160 | };
161 | }
162 |
163 | export interface Album {
164 | id: string;
165 | slug: string;
166 | title: string;
167 | metadata: {
168 | cover: {
169 | imgix_url: string;
170 | };
171 | artist: {
172 | title: string;
173 | };
174 | };
175 | }
176 |
177 | export interface Playlist {
178 | id: string;
179 | title: string;
180 | metadata: {
181 | description: string;
182 | cover: {
183 | imgix_url: string;
184 | };
185 | tracks: Track[];
186 | };
187 | }
188 | ```
189 |
190 | ## Configuring Cosmic Connection
191 |
192 | Create `lib/cosmic.ts`:
193 |
194 | ```typescript
195 | import { createBucketClient } from "@cosmicjs/sdk";
196 |
197 | const cosmic = createBucketClient({
198 | bucketSlug: process.env.COSMIC_BUCKET_SLUG as string,
199 | readKey: process.env.COSMIC_READ_KEY as string,
200 | });
201 |
202 | export default cosmic;
203 | ```
204 |
205 | ## Seeding the CMS
206 |
207 | Create `scripts/seed-cosmic.ts` to set up our object types and sample content:
208 |
209 | ```typescript
210 | import { createBucketClient } from "@cosmicjs/sdk";
211 |
212 | const BUCKET_SLUG = process.env.COSMIC_BUCKET_SLUG;
213 | const WRITE_KEY = process.env.COSMIC_WRITE_KEY;
214 | const READ_KEY = process.env.COSMIC_READ_KEY;
215 |
216 | if (!BUCKET_SLUG || !WRITE_KEY || !READ_KEY) {
217 | throw new Error("Missing required environment variables");
218 | }
219 |
220 | const cosmic = createBucketClient({
221 | bucketSlug: BUCKET_SLUG,
222 | writeKey: WRITE_KEY,
223 | readKey: READ_KEY,
224 | });
225 |
226 | // Helper functions for uploading media
227 | async function uploadImageFromUrl(url: string, filename: string) {
228 | const response = await fetch(url);
229 | const blob = await response.blob();
230 | const buffer = await blob.arrayBuffer();
231 |
232 | const { media } = await cosmic.media.insertOne({
233 | media: {
234 | originalname: filename,
235 | buffer: Buffer.from(buffer),
236 | },
237 | });
238 |
239 | return media;
240 | }
241 |
242 | async function uploadAudioFromUrl(url: string, filename: string) {
243 | const response = await fetch(url);
244 | const blob = await response.blob();
245 | const buffer = await blob.arrayBuffer();
246 |
247 | const { media } = await cosmic.media.insertOne({
248 | media: {
249 | originalname: filename,
250 | buffer: Buffer.from(buffer),
251 | },
252 | });
253 |
254 | return media;
255 | }
256 |
257 | // Create object types
258 | async function seedObjectTypes() {
259 | // Create Artists object type
260 | await cosmic.objectTypes.insertOne({
261 | title: "Artists",
262 | slug: "artists",
263 | singular: "Artist",
264 | emoji: "👤",
265 | metafields: [
266 | {
267 | title: "Name",
268 | key: "name",
269 | type: "text",
270 | required: true,
271 | },
272 | {
273 | title: "Image",
274 | key: "image",
275 | type: "file",
276 | required: true,
277 | media_validation_type: "image",
278 | },
279 | {
280 | title: "Bio",
281 | key: "bio",
282 | type: "textarea",
283 | required: true,
284 | },
285 | ],
286 | });
287 |
288 | // Create Albums object type
289 | await cosmic.objectTypes.insertOne({
290 | title: "Albums",
291 | slug: "albums",
292 | singular: "Album",
293 | emoji: "💿",
294 | metafields: [
295 | {
296 | title: "Title",
297 | key: "title",
298 | type: "text",
299 | required: true,
300 | },
301 | {
302 | title: "Cover",
303 | key: "cover",
304 | type: "file",
305 | required: true,
306 | media_validation_type: "image",
307 | },
308 | {
309 | title: "Release Date",
310 | key: "release_date",
311 | type: "date",
312 | required: true,
313 | },
314 | {
315 | title: "Artist",
316 | key: "artist",
317 | type: "object",
318 | object_type: "artists",
319 | required: true,
320 | },
321 | ],
322 | });
323 |
324 | // Create Tracks object type
325 | await cosmic.objectTypes.insertOne({
326 | title: "Tracks",
327 | slug: "tracks",
328 | singular: "Track",
329 | emoji: "🎵",
330 | metafields: [
331 | {
332 | title: "Title",
333 | key: "title",
334 | type: "text",
335 | required: true,
336 | },
337 | {
338 | title: "Audio File",
339 | key: "audio",
340 | type: "file",
341 | required: true,
342 | media_validation_type: "audio",
343 | },
344 | {
345 | title: "Duration",
346 | key: "duration",
347 | type: "number",
348 | required: true,
349 | },
350 | {
351 | title: "Album",
352 | key: "album",
353 | type: "object",
354 | object_type: "albums",
355 | required: true,
356 | },
357 | ],
358 | });
359 |
360 | // Create Playlists object type
361 | await cosmic.objectTypes.insertOne({
362 | title: "Playlists",
363 | slug: "playlists",
364 | singular: "Playlist",
365 | emoji: "📀",
366 | metafields: [
367 | {
368 | title: "Title",
369 | key: "title",
370 | type: "text",
371 | required: true,
372 | },
373 | {
374 | title: "Description",
375 | key: "description",
376 | type: "textarea",
377 | required: true,
378 | },
379 | {
380 | title: "Cover",
381 | key: "cover",
382 | type: "file",
383 | required: true,
384 | media_validation_type: "image",
385 | },
386 | {
387 | title: "Tracks",
388 | key: "tracks",
389 | type: "objects",
390 | object_type: "tracks",
391 | required: false,
392 | },
393 | ],
394 | });
395 | }
396 |
397 | // Create sample content
398 | async function seedContent() {
399 | // Upload artist images
400 | const lunaImage = await uploadImageFromUrl(
401 | "https://images.unsplash.com/photo-1494354145959-25cb82edf23d?w=400&h=400&fit=crop",
402 | "luna-moon.jpg"
403 | );
404 | const novaImage = await uploadImageFromUrl(
405 | "https://images.unsplash.com/photo-1516223725307-6f76b9ec8742?w=400&h=400&fit=crop",
406 | "nova-star.jpg"
407 | );
408 |
409 | // Create sample artists
410 | const { object: artist1 } = await cosmic.objects.insertOne({
411 | title: "Luna Moon",
412 | slug: "luna-moon",
413 | type: "artists",
414 | thumbnail: lunaImage.name,
415 | metadata: {
416 | name: "Luna Moon",
417 | bio: "Luna Moon is a cosmic pop sensation known for her ethereal vocals and space-themed music. Her unique blend of electronic and acoustic elements has created a new genre called 'astro-pop'.",
418 | image: lunaImage.name,
419 | },
420 | });
421 |
422 | const { object: artist2 } = await cosmic.objects.insertOne({
423 | title: "Nova Star",
424 | slug: "nova-star",
425 | type: "artists",
426 | thumbnail: novaImage.name,
427 | metadata: {
428 | name: "Nova Star",
429 | bio: "Nova Star is an indie rock phenomenon who writes songs about quantum physics and parallel universes. His experimental sound has earned him the nickname 'The Einstein of Rock'.",
430 | image: novaImage.name,
431 | },
432 | });
433 |
434 | // Upload album covers
435 | const starlightCover = await uploadImageFromUrl(
436 | "https://images.unsplash.com/photo-1419242902214-272b3f66ee7a?w=400&h=400&fit=crop",
437 | "starlight.jpg"
438 | );
439 | const quantumCover = await uploadImageFromUrl(
440 | "https://images.unsplash.com/photo-1557264337-e8a93017fe92?w=400&h=400&fit=crop",
441 | "quantum.jpg"
442 | );
443 |
444 | // Create sample albums
445 | const { object: album1 } = await cosmic.objects.insertOne({
446 | title: "Starlight Symphony",
447 | slug: "starlight-symphony",
448 | type: "albums",
449 | thumbnail: starlightCover.name,
450 | metadata: {
451 | title: "Starlight Symphony",
452 | release_date: "2023-06-15",
453 | artist: artist1.id,
454 | cover: starlightCover.name,
455 | },
456 | });
457 |
458 | const { object: album2 } = await cosmic.objects.insertOne({
459 | title: "Quantum Dreams",
460 | slug: "quantum-dreams",
461 | type: "albums",
462 | thumbnail: quantumCover.name,
463 | metadata: {
464 | title: "Quantum Dreams",
465 | release_date: "2023-08-22",
466 | artist: artist2.id,
467 | cover: quantumCover.name,
468 | },
469 | });
470 |
471 | // Create sample tracks
472 | const cosmicDanceAudio = await uploadAudioFromUrl(
473 | "https://cdn.cosmicjs.com/1474f620-05be-11f0-993b-3bd041905fff-relaxing-jazz-saxophone-music-saxophone-instruments-music-303093.mp3",
474 | "cosmic-dance.mp3"
475 | );
476 |
477 | const parallelWorldsAudio = await uploadAudioFromUrl(
478 | "https://cdn.cosmicjs.com/147e44f0-05be-11f0-993b-3bd041905fff-iced-coffee-jazz-309947.mp3",
479 | "parallel-worlds.mp3"
480 | );
481 |
482 | const { object: track1 } = await cosmic.objects.insertOne({
483 | title: "Cosmic Dance",
484 | slug: "cosmic-dance",
485 | type: "tracks",
486 | metadata: {
487 | title: "Cosmic Dance",
488 | duration: 245,
489 | album: album1.id,
490 | audio: cosmicDanceAudio.name,
491 | },
492 | });
493 |
494 | const { object: track2 } = await cosmic.objects.insertOne({
495 | title: "Parallel Worlds",
496 | slug: "parallel-worlds",
497 | type: "tracks",
498 | metadata: {
499 | title: "Parallel Worlds",
500 | duration: 312,
501 | album: album2.id,
502 | audio: parallelWorldsAudio.name,
503 | },
504 | });
505 |
506 | // Upload playlist cover
507 | const playlistCover = await uploadImageFromUrl(
508 | "https://images.unsplash.com/photo-1470225620780-dba8ba36b745?w=400&h=400&fit=crop",
509 | "cosmic-hits.jpg"
510 | );
511 |
512 | // Create sample playlist
513 | await cosmic.objects.insertOne({
514 | title: "Cosmic Hits 2023",
515 | slug: "cosmic-hits-2023",
516 | type: "playlists",
517 | thumbnail: playlistCover.name,
518 | metadata: {
519 | title: "Cosmic Hits 2023",
520 | description:
521 | "A stellar collection of the year's most out-of-this-world tracks",
522 | tracks: [track1.id, track2.id],
523 | cover: playlistCover.name,
524 | },
525 | });
526 | }
527 |
528 | // Run both seeding functions
529 | async function seed() {
530 | await seedObjectTypes();
531 | await seedContent();
532 | }
533 |
534 | seed();
535 | ```
536 |
537 | Run the seed script to set up your Cosmic bucket:
538 |
539 | ```bash
540 | npx ts-node scripts/seed-cosmic.ts
541 | ```
542 |
543 | ## Building the Application
544 |
545 | ### 1. Create the Root Layout
546 |
547 | Create `app/layout.tsx`:
548 |
549 | ```typescript
550 | import type { Metadata } from "next";
551 | import { Inter } from "next/font/google";
552 | import "./globals.css";
553 | import { PlayerProvider } from "@/lib/PlayerContext";
554 | import Navigation from "@/components/Navigation";
555 | import Footer from "@/components/Footer";
556 |
557 | const inter = Inter({ subsets: ["latin"] });
558 |
559 | export const metadata: Metadata = {
560 | title: "Spotify Clone - Built with Cosmic",
561 | description: "A demo app showing what's possible with Cosmic",
562 | };
563 |
564 | export default function RootLayout({
565 | children,
566 | }: Readonly<{
567 | children: React.ReactNode;
568 | }>) {
569 | return (
570 |
571 |
572 |
573 |
574 | {children}
575 |
576 |
577 |
578 |
579 | );
580 | }
581 | ```
582 |
583 | ### 2. Create the Player Context
584 |
585 | Create `lib/PlayerContext.tsx`:
586 |
587 | ```typescript
588 | "use client";
589 |
590 | import { createContext, useContext, useState, ReactNode } from "react";
591 | import MusicPlayer from "@/components/MusicPlayer";
592 | import { Track } from "@/types";
593 |
594 | interface PlayerContextType {
595 | currentTrack: Track | null;
596 | setCurrentTrack: (track: Track | null) => void;
597 | }
598 |
599 | const PlayerContext = createContext
(undefined);
600 |
601 | export function PlayerProvider({ children }: { children: ReactNode }) {
602 | const [currentTrack, setCurrentTrack] = useState