Kurs vibe coding z AI · Moduł 14: Pinterest Clone
Visual Search i Shopping Integration
W tej lekcji3
Pinterest to discovery through visual search i shopping.
Visual Search
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. Jak działa visual search w Pinterest?
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: