Kurs vibe coding z AI · Moduł 11: Spotify Clone

Deployment - Vercel/Netlify

1 min czytania
W tej lekcji3

Deployment Next.js app do produkcji.

Vercel Deployment

1. GitHub Integration

  1. Pushuj kod na GitHub
  2. Idź na vercel.com
  3. Import GitHub repository
  4. Click Deploy

2. Environment Variables

W Vercel dashboard → Settings → Environment Variables:

DATABASE_URL=your_db_url
OPENAI_API_KEY=your_key
NEXTAUTH_SECRET=your_secret

3. Custom Domain

Settings → Domains → Add domain → Verify DNS

CLI Deployment

npm i -g vercel
vercel login
vercel

Build 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. 1. Jak wdrożyć aplikację Next.js na Vercel przez integrację z GitHub?

  2. 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:

Przydatne artykuły