Vibe coding with AI course · Module 11: Spotify Clone

Deployment - Vercel/Netlify

1 min read
In this lesson3

Deploying a Next.js app to production.

Vercel Deployment

1. GitHub Integration

  1. Push your code to GitHub
  2. Go to vercel.com
  3. Import GitHub repository
  4. Click Deploy

2. Environment Variables

In 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
}

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. 1. How do you deploy a Next.js app to Vercel via GitHub integration?

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

Useful articles