Kurs vibe coding z AI · Moduł 11: Spotify Clone
Deployment - Vercel/Netlify
W tej lekcji3
Deployment Next.js app do produkcji.
Vercel Deployment
1. GitHub Integration
- Pushuj kod na GitHub
- Idź na vercel.com
- Import GitHub repository
- Click Deploy
2. Environment Variables
W 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
}Do zobaczenia!
Kod do tej lekcji: 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: "Hymn piratów",
artist: 'Kapitan Redbeard',
coverArt: 'https://images.unsplash.com/photo-1470225620780-dba8ba36b745?w=200&h=200&fit=crop',
duration: 240
},
{
id: '2',
title: 'Żeglarska przygoda',
artist: 'Kapitan Redbeard',
coverArt: 'https://images.unsplash.com/photo-1493225457124-a3eb161ffa5f?w=200&h=200&fit=crop',
duration: 195
}
];
const mockArtists: Artist[] = [
{
id: '1',
name: 'Kapitan Redbeard',
avatar: 'https://i.pravatar.cc/150?img=12'
},
{
id: '2',
name: 'Rada Załogi',
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">
{/* Pole wyszukiwania */}
<div className="mb-6">
<div className="relative max-w-md">
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Czego chcesz posłuchać?"
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">Przeglądaj wszystko</h2>
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
{['Szanty', 'Hymny bitewne', 'Łowy na skarby', 'Spokojne morza', 'Epickie przygody', 'Naprawy statku'].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>
) : (
<>
{/* Zakładki */}
<div className="flex gap-4 mb-6 border-b border-gray-800">
{[
{ key: 'all', label: 'Wszystko' },
{ key: 'tracks', label: 'Utwory' },
{ key: 'artists', label: 'Wykonawcy' }
].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>
{/* Wyniki */}
<div className="space-y-6">
{(activeTab === 'all' || activeTab === 'tracks') && filteredTracks.length > 0 && (
<div>
<h3 className="text-xl font-bold mb-4">Utwory</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">Wykonawcy</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">Wykonawca</div>
</div>
))}
</div>
</div>
)}
</div>
</>
)}
</div>
);
}Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Jak wdrożyć aplikację Next.js na Vercel przez integrację z GitHub?
2. Gdzie w Vercel ustawiamy zmienne środowiskowe (np. DATABASE_URL)?
To 2 z 3 pytań do tej lekcji. Pozostałe rozwiążesz w grze.
Zadania praktyczne w grze
- Klikanie w kolejności
Kliknij elementy, aby zbudować komendę instalacji Vercel CLI:
- Układanie w pionie
Uporządkuj kroki wdrożenia aplikacji przez Vercel CLI: