Vibe coding with AI course · Module 11: Spotify Clone
Deployment - Vercel/Netlify
In this lesson3
Deploying a Next.js app to production.
Vercel Deployment
1. GitHub Integration
- Push your code to GitHub
- Go to vercel.com
- Import GitHub repository
- Click Deploy
2. Environment Variables
In Vercel dashboard → Settings → Environment Variables:
DATABASE_URL=your_db_url
OPENAI_API_KEY=your_key
NEXTAUTH_SECRET=your_secret3. Custom Domain
Settings → Domains → Add domain → Verify DNS
CLI Deployment
npm i -g vercel
vercel login
vercelBuild Optimization
// next.config.js
module.exports = {
images: {
domains: ['yourdomain.com'],
formats: ['image/avif', 'image/webp']
},
compress: true,
poweredByHeader: false
}See you soon!
Code for this lesson: App.tsx
'use client';
import { useState, useEffect } from 'react';
interface Track {
id: string;
title: string;
artist: string;
coverArt: string;
duration: number;
}
interface Artist {
id: string;
name: string;
avatar: string;
}
const mockTracks: Track[] = [
{
id: '1',
title: "The Pirate's Anthem",
artist: 'Captain Redbeard',
coverArt: 'https://images.unsplash.com/photo-1470225620780-dba8ba36b745?w=200&h=200&fit=crop',
duration: 240
},
{
id: '2',
title: 'Sailing Adventure',
artist: 'Captain Redbeard',
coverArt: 'https://images.unsplash.com/photo-1493225457124-a3eb161ffa5f?w=200&h=200&fit=crop',
duration: 195
}
];
const mockArtists: Artist[] = [
{
id: '1',
name: 'Captain Redbeard',
avatar: 'https://i.pravatar.cc/150?img=12'
},
{
id: '2',
name: 'Crew Council',
avatar: 'https://i.pravatar.cc/150?img=33'
}
];
export default function SearchPage() {
const [query, setQuery] = useState('');
const [activeTab, setActiveTab] = useState<'all' | 'tracks' | 'artists'>('all');
const [filteredTracks, setFilteredTracks] = useState<Track[]>([]);
const [filteredArtists, setFilteredArtists] = useState<Artist[]>([]);
useEffect(() => {
if (query.length > 0) {
const timeout = setTimeout(() => {
setFilteredTracks(
mockTracks.filter(t =>
t.title.toLowerCase().includes(query.toLowerCase()) ||
t.artist.toLowerCase().includes(query.toLowerCase())
)
);
setFilteredArtists(
mockArtists.filter(a =>
a.name.toLowerCase().includes(query.toLowerCase())
)
);
}, 300);
return () => clearTimeout(timeout);
} else {
setFilteredTracks([]);
setFilteredArtists([]);
}
}, [query]);
return (
<div className="min-h-screen bg-black text-white p-8">
{/* Search Input */}
<div className="mb-6">
<div className="relative max-w-md">
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="What do you want to listen to?"
className="w-full bg-gray-800 text-white px-12 py-3 rounded-full outline-none focus:ring-2 focus:ring-white"
autoFocus
/>
<span className="absolute left-4 top-1/2 transform -translate-y-1/2 text-xl">
⌕
</span>
</div>
</div>
{!query ? (
<div>
<h2 className="text-2xl font-bold mb-4">Browse All</h2>
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
{['Sea Shanties', 'Battle Anthems', 'Treasure Hunting', 'Calm Seas', 'Epic Adventures', 'Ship Repairs'].map(genre => (
<button
key={genre}
className="aspect-square bg-gradient-to-br from-purple-600 to-pink-600 rounded-lg p-4 font-bold text-lg hover:scale-105 transition"
>
{genre}
</button>
))}
</div>
</div>
) : (
<>
{/* Tabs */}
<div className="flex gap-4 mb-6 border-b border-gray-800">
{[
{ key: 'all', label: 'All' },
{ key: 'tracks', label: 'Songs' },
{ key: 'artists', label: 'Artists' }
].map(tab => (
<button
key={tab.key}
onClick={() => setActiveTab(tab.key as any)}
className={`pb-2 px-4 font-semibold ${
activeTab === tab.key
? 'border-b-2 border-green-500'
: 'text-gray-400'
}`}
>
{tab.label}
</button>
))}
</div>
{/* Results */}
<div className="space-y-6">
{(activeTab === 'all' || activeTab === 'tracks') && filteredTracks.length > 0 && (
<div>
<h3 className="text-xl font-bold mb-4">Songs</h3>
<div className="space-y-2">
{filteredTracks.map(track => (
<div
key={track.id}
className="flex items-center gap-4 p-2 rounded hover:bg-gray-800 cursor-pointer"
>
<img src={track.coverArt} alt={track.title} className="w-12 h-12 rounded" />
<div className="flex-1">
<div className="font-semibold">{track.title}</div>
<div className="text-sm text-gray-400">{track.artist}</div>
</div>
<span className="text-sm text-gray-400">
{Math.floor(track.duration / 60)}:{(track.duration % 60).toString().padStart(2, '0')}
</span>
</div>
))}
</div>
</div>
)}
{(activeTab === 'all' || activeTab === 'artists') && filteredArtists.length > 0 && (
<div>
<h3 className="text-xl font-bold mb-4">Artists</h3>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{filteredArtists.map(artist => (
<div
key={artist.id}
className="bg-gray-900 p-4 rounded hover:bg-gray-800 cursor-pointer"
>
<img src={artist.avatar} alt={artist.name} className="w-full aspect-square rounded-full mb-4" />
<div className="font-semibold">{artist.name}</div>
<div className="text-sm text-gray-400">Artist</div>
</div>
))}
</div>
</div>
)}
</div>
</>
)}
</div>
);
}Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. How do you deploy a Next.js app to Vercel via GitHub integration?
2. Where in Vercel do you set environment variables (e.g. DATABASE_URL)?
These are 2 of 3 questions for this lesson. Solve the rest in the game.
Hands-on tasks in the game
- Click in order
Click the elements to build the Vercel CLI install command:
- Vertical ordering
Order the steps of deploying the app via the Vercel CLI: