Vibe coding with AI course Β· Module 14: Pinterest Clone
Visual Search and Shopping Integration
In this lesson3
Pinterest is all about discovery through visual search and 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>
);
}Summary
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
You now have a complete Pinterest clone with masonry grid, boards, visual search and shopping!
Congratulations! You've completed all 14 modules of Vibecoding!
See you there!
Code for this lesson: 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(`Created board: "${name}" (${isPrivate ? 'Secret' : 'Public'})`);
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"
>
Create Board
</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">Create board</h2>
<form onSubmit={handleSubmit}>
<div className="mb-4">
<label className="block text-sm font-semibold mb-2">Name</label>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder='Like "Travel" or "Recipes"'
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">Keep this board secret</div>
<div className="text-sm text-gray-600">Only you can see this board</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"
>
Cancel
</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"
>
Create
</button>
</div>
</form>
</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. How does visual search work in Pinterest?
2. What does the VisualSearchResult object returned by visualSearch contain?
These are 2 of 9 questions for this lesson. Solve the rest in the game.
Hands-on tasks in the game
- Vertical ordering
Order the steps of CameraSearch after uploading an image:
- Horizontal ordering
Arrange the elements of one line of the price declaration in the Product interface:
- Click in order
Click the elements to build the reading of the uploaded image as a Data URL:
- Code editor
Create a board page
- Vertical ordering
Order the ShoppingPinCard elements from top to bottom:
- Click in order
Click the elements to build the formatting of the product rating to one decimal place:
- Horizontal ordering
Arrange the elements of one line of the price badge on a shopping pin card:
- Code editor
Create a visual search interface
- Vertical ordering
Order the full visual search flow from image to purchase: