Kurs vibe coding z AI · Moduł 14: Pinterest Clone

Visual Search i Shopping Integration

3 min czytania
W tej lekcji3

Pinterest to discovery through visual search i shopping.

interface VisualSearchResult {
  similarPins: Pin[];
  relatedSearches: string[];
  shoppingResults?: Product[];
}

// Search by image
async function visualSearch(imageUrl: string): Promise<VisualSearchResult> {
  // Send image to computer vision API
  // Returns similar images and detected objects/colors

  return {
    similarPins: await findSimilarPins(imageUrl),
    relatedSearches: await extractKeywords(imageUrl),
    shoppingResults: await findProducts(imageUrl)
  };
}

// Camera search component
'use client';

export default function CameraSearch() {
  const [imagePreview, setImagePreview] = useState<string | null>(null);
  const [results, setResults] = useState<VisualSearchResult | null>(null);
  const [isSearching, setIsSearching] = useState(false);

  const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    if (!file) return;

    const reader = new FileReader();
    reader.onload = (event) => {
      const imageUrl = event.target?.result as string;
      setImagePreview(imageUrl);
      performVisualSearch(imageUrl);
    };
    reader.readAsDataURL(file);
  };

  const performVisualSearch = async (imageUrl: string) => {
    setIsSearching(true);
    const searchResults = await visualSearch(imageUrl);
    setResults(searchResults);
    setIsSearching(false);
  };

  return (
    <div className="min-h-screen bg-white p-8">
      <div className="max-w-6xl mx-auto">
        {/* Upload Area */}
        {!imagePreview ? (
          <div className="text-center">
            <h1 className="text-4xl font-bold mb-8">Search by image</h1>

            <label className="inline-block cursor-pointer">
              <div className="w-64 h-64 border-4 border-dashed border-gray-300 rounded-2xl flex flex-col items-center justify-center hover:border-red-500 transition">
                <div className="text-6xl mb-4">◆</div>
                <div className="font-semibold mb-2">Upload an image</div>
                <div className="text-sm text-gray-600">or drag and drop</div>
              </div>
              <input
                type="file"
                accept="image/*"
                onChange={handleImageUpload}
                className="hidden"
              />
            </label>
          </div>
        ) : (
          <div>
            {/* Image Preview */}
            <div className="mb-8">
              <img
                src={imagePreview}
                alt="Search query"
                className="max-w-sm mx-auto rounded-2xl shadow-lg"
              />
            </div>

            {/* Loading */}
            {isSearching && (
              <div className="text-center text-gray-600 mb-8">
                Searching for similar pins...
              </div>
            )}

            {/* Results */}
            {results && (
              <div>
                {/* Related Searches */}
                <div className="mb-8">
                  <h2 className="text-2xl font-bold mb-4">Related searches</h2>
                  <div className="flex flex-wrap gap-2">
                    {results.relatedSearches.map(search => (
                      <button
                        key={search}
                        className="px-4 py-2 bg-gray-200 rounded-full hover:bg-gray-300"
                      >
                        {search}
                      </button>
                    ))}
                  </div>
                </div>

                {/* Similar Pins */}
                <div>
                  <h2 className="text-2xl font-bold mb-4">Similar pins</h2>
                  <MasonryGrid pins={results.similarPins} />
                </div>
              </div>
            )}
          </div>
        )}
      </div>
    </div>
  );
}

Shopping Integration

interface Product {
  id: string;
  name: string;
  price: number;
  currency: string;
  imageUrl: string;
  merchant: {
    name: string;
    logo: string;
  };
  inStock: boolean;
  rating?: number;
  reviewCount?: number;
}

// Product pin
interface ProductPin extends Pin {
  product: Product;
  isShoppable: true;
}

// Shopping pin card
function ShoppingPinCard({ pin }: { pin: ProductPin }) {
  return (
    <div className="relative rounded-2xl overflow-hidden group cursor-pointer">
      <img src={pin.imageUrl} alt={pin.title} className="w-full h-auto" />

      {/* Price Badge */}
      <div className="absolute top-4 left-4 px-3 py-1 bg-white rounded-full font-bold shadow-lg">
        ${pin.product.price}
      </div>

      {/* Hover Overlay */}
      <div className="absolute inset-0 bg-black/40 p-4 flex flex-col justify-between opacity-0 group-hover:opacity-100 transition">
        <div className="flex justify-end gap-2">
          <button className="px-4 py-2 bg-white rounded-full font-semibold hover:bg-gray-100">
            Save
          </button>
          <button className="px-4 py-2 bg-blue-600 text-white rounded-full font-semibold hover:bg-blue-700">
            Shop
          </button>
        </div>

        <div className="bg-white p-3 rounded-2xl">
          <div className="flex items-center gap-2 mb-2">
            <img
              src={pin.product.merchant.logo}
              alt={pin.product.merchant.name}
              className="w-6 h-6 rounded"
            />
            <span className="text-sm font-semibold">
              {pin.product.merchant.name}
            </span>
          </div>

          <h3 className="font-bold mb-1">{pin.product.name}</h3>

          {pin.product.rating && (
            <div className="flex items-center gap-1 text-sm">
              <span>{pin.product.rating.toFixed(1)}</span>
              <span className="text-gray-600">
                ({pin.product.reviewCount} reviews)
              </span>
            </div>
          )}

          {!pin.product.inStock && (
            <div className="text-red-600 text-sm font-semibold mt-2">
              Out of stock
            </div>
          )}
        </div>
      </div>
    </div>
  );
}

Podsumowanie

Visual search - upload image to find similar pins Image upload - drag and drop or file picker Related searches - keyword extraction from image Similar pins - computer vision matching Shopping pins - product integration Price badge - show price on pins Merchant info - seller details Shop button - direct purchase link Stock status - availability indicator

Masz teraz kompletny Pinterest clone z masonry grid, boards, visual search i shopping!

Gratulacje! Ukończyłeś wszystkie 14 modułów Vibecoding!

Do zobaczenia!

Kod do tej lekcji: App.tsx
'use client';

import { useState } from 'react';

export default function CreateBoardModal() {
  const [isOpen, setIsOpen] = useState(true);
  const [name, setName] = useState('');
  const [isPrivate, setIsPrivate] = useState(false);

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    if (name.trim()) {
      alert(`Utworzono tablicę: "${name}" (${isPrivate ? 'tajna' : 'publiczna'})`);
      setIsOpen(false);
    }
  };

  if (!isOpen) {
    return (
      <div className="min-h-screen bg-gray-100 flex items-center justify-center">
        <button
          onClick={() => setIsOpen(true)}
          className="px-6 py-3 bg-red-600 text-white rounded-full font-semibold hover:bg-red-700"
        >
          Utwórz tablicę
        </button>
      </div>
    );
  }

  return (
    <div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
      <div className="bg-white rounded-2xl max-w-md w-full p-6">
        <h2 className="text-2xl font-bold mb-6">Utwórz tablicę</h2>

        <form onSubmit={handleSubmit}>
          <div className="mb-4">
            <label className="block text-sm font-semibold mb-2">Nazwa</label>
            <input
              type="text"
              value={name}
              onChange={(e) => setName(e.target.value)}
              placeholder='Na przykład "Podróże" albo "Przepisy"'
              className="w-full px-4 py-3 border-2 border-gray-200 rounded-2xl outline-none focus:border-blue-500"
              autoFocus
            />
          </div>

          <div className="mb-6">
            <label className="flex items-start gap-3 cursor-pointer">
              <input
                type="checkbox"
                checked={isPrivate}
                onChange={(e) => setIsPrivate(e.target.checked)}
                className="w-5 h-5 mt-1"
              />
              <div>
                <div className="font-semibold">Zachowaj tę tablicę jako tajną</div>
                <div className="text-sm text-gray-600">Tylko Ty widzisz tę tablicę</div>
              </div>
            </label>
          </div>

          <div className="flex gap-3">
            <button
              type="button"
              onClick={() => setIsOpen(false)}
              className="flex-1 py-3 border-2 border-gray-200 rounded-full font-semibold hover:border-gray-300"
            >
              Anuluj
            </button>
            <button
              type="submit"
              disabled={!name.trim()}
              className="flex-1 py-3 bg-red-600 text-white rounded-full font-semibold hover:bg-red-700 disabled:bg-gray-300"
            >
              Utwórz
            </button>
          </div>
        </form>
      </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 działa visual search w Pinterest?

  2. 2. Co zawiera obiekt VisualSearchResult zwracany przez visualSearch?

To 2 z 9 pytań do tej lekcji. Pozostałe rozwiążesz w grze.

Zadania praktyczne w grze

  • Układanie w pionie

    Uporządkuj etapy działania CameraSearch po wgraniu obrazu:

  • Układanie w poziomie

    Ułóż elementy jednej linii deklaracji ceny w interfejsie Product:

  • Klikanie w kolejności

    Kliknij elementy, aby zbudować odczyt wgranego obrazu jako Data URL:

  • Edytor kodu

    Stwórz stronę board

  • Układanie w pionie

    Uporządkuj elementy ShoppingPinCard od góry do dołu:

  • Klikanie w kolejności

    Kliknij elementy, aby zbudować formatowanie oceny produktu do jednego miejsca po przecinku:

  • Układanie w poziomie

    Ułóż elementy jednej linii price badge na shopping pin card:

  • Edytor kodu

    Stwórz visual search interface

  • Układanie w pionie

    Uporządkuj pełny przepływ wyszukiwania wizualnego od obrazu do zakupu:

Przydatne artykuły