Cours Vibe coding avec l'IA · Module 14: Clone de Pinterest
Recherche visuelle et intégration du shopping
Dans cette leçon3
Pinterest, c'est avant tout la découverte par la recherche visuelle et le shopping.
Recherche visuelle
1interface VisualSearchResult {
2 similarPins: Pin[];
3 relatedSearches: string[];
4 shoppingResults?: Product[];
5}
6
7// Recherche par image
8async function visualSearch(imageUrl: string): Promise<VisualSearchResult> {
9 // Envoyer l'image à une API de vision par ordinateur
10 // Renvoie des images similaires et les objets/couleurs détectés
11
12 return {
13 similarPins: await findSimilarPins(imageUrl),
14 relatedSearches: await extractKeywords(imageUrl),
15 shoppingResults: await findProducts(imageUrl)
16 };
17}
18
19// Composant de recherche par appareil photo
20'use client';
21
22export default function CameraSearch() {
23 const [imagePreview, setImagePreview] = useState<string | null>(null);
24 const [results, setResults] = useState<VisualSearchResult | null>(null);
25 const [isSearching, setIsSearching] = useState(false);
26
27 const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
28 const file = e.target.files?.[0];
29 if (!file) return;
30
31 const reader = new FileReader();
32 reader.onload = (event) => {
33 const imageUrl = event.target?.result as string;
34 setImagePreview(imageUrl);
35 performVisualSearch(imageUrl);
36 };
37 reader.readAsDataURL(file);
38 };
39
40 const performVisualSearch = async (imageUrl: string) => {
41 setIsSearching(true);
42 const searchResults = await visualSearch(imageUrl);
43 setResults(searchResults);
44 setIsSearching(false);
45 };
46
47 return (
48 <div className="min-h-screen bg-white p-8">
49 <div className="max-w-6xl mx-auto">
50 {/* Upload Area */}
51 {!imagePreview ? (
52 <div className="text-center">
53 <h1 className="text-4xl font-bold mb-8">Rechercher par image</h1>
54
55 <label className="inline-block cursor-pointer">
56 <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">
57 <div className="text-6xl mb-4">◆</div>
58 <div className="font-semibold mb-2">Téléverser une image</div>
59 <div className="text-sm text-gray-600">ou glisser-déposer</div>
60 </div>
61 <input
62 type="file"
63 accept="image/*"
64 onChange={handleImageUpload}
65 className="hidden"
66 />
67 </label>
68 </div>
69 ) : (
70 <div>
71 {/* Image Preview */}
72 <div className="mb-8">
73 <img
74 src={imagePreview}
75 alt="Search query"
76 className="max-w-sm mx-auto rounded-2xl shadow-lg"
77 />
78 </div>
79
80 {/* Loading */}
81 {isSearching && (
82 <div className="text-center text-gray-600 mb-8">
83 Recherche de pins similaires...
84 </div>
85 )}
86
87 {/* Results */}
88 {results && (
89 <div>
90 {/* Related Searches */}
91 <div className="mb-8">
92 <h2 className="text-2xl font-bold mb-4">Recherches associées</h2>
93 <div className="flex flex-wrap gap-2">
94 {results.relatedSearches.map(search => (
95 <button
96 key={search}
97 className="px-4 py-2 bg-gray-200 rounded-full hover:bg-gray-300"
98 >
99 {search}
100 </button>
101 ))}
102 </div>
103 </div>
104
105 {/* Similar Pins */}
106 <div>
107 <h2 className="text-2xl font-bold mb-4">Pins similaires</h2>
108 <MasonryGrid pins={results.similarPins} />
109 </div>
110 </div>
111 )}
112 </div>
113 )}
114 </div>
115 </div>
116 );
117}Intégration du shopping
1interface Product {
2 id: string;
3 name: string;
4 price: number;
5 currency: string;
6 imageUrl: string;
7 merchant: {
8 name: string;
9 logo: string;
10 };
11 inStock: boolean;
12 rating?: number;
13 reviewCount?: number;
14}
15
16// Pin produit
17interface ProductPin extends Pin {
18 product: Product;
19 isShoppable: true;
20}
21
22// Carte de pin shopping
23function ShoppingPinCard({ pin }: { pin: ProductPin }) {
24 return (
25 <div className="relative rounded-2xl overflow-hidden group cursor-pointer">
26 <img src={pin.imageUrl} alt={pin.title} className="w-full h-auto" />
27
28 {/* Price Badge */}
29 <div className="absolute top-4 left-4 px-3 py-1 bg-white rounded-full font-bold shadow-lg">
30 ${pin.product.price}
31 </div>
32
33 {/* Hover Overlay */}
34 <div className="absolute inset-0 bg-black/40 p-4 flex flex-col justify-between opacity-0 group-hover:opacity-100 transition">
35 <div className="flex justify-end gap-2">
36 <button className="px-4 py-2 bg-white rounded-full font-semibold hover:bg-gray-100">
37 Enregistrer
38 </button>
39 <button className="px-4 py-2 bg-blue-600 text-white rounded-full font-semibold hover:bg-blue-700">
40 Acheter
41 </button>
42 </div>
43
44 <div className="bg-white p-3 rounded-2xl">
45 <div className="flex items-center gap-2 mb-2">
46 <img
47 src={pin.product.merchant.logo}
48 alt={pin.product.merchant.name}
49 className="w-6 h-6 rounded"
50 />
51 <span className="text-sm font-semibold">
52 {pin.product.merchant.name}
53 </span>
54 </div>
55
56 <h3 className="font-bold mb-1">{pin.product.name}</h3>
57
58 {pin.product.rating && (
59 <div className="flex items-center gap-1 text-sm">
60 <span>{pin.product.rating.toFixed(1)}</span>
61 <span className="text-gray-600">
62 ({pin.product.reviewCount} avis)
63 </span>
64 </div>
65 )}
66
67 {!pin.product.inStock && (
68 <div className="text-red-600 text-sm font-semibold mt-2">
69 Rupture de stock
70 </div>
71 )}
72 </div>
73 </div>
74 </div>
75 );
76}Résumé
Recherche visuelle - téléverser une image pour trouver des pins similaires Téléversement d'image - glisser-déposer ou sélecteur de fichier Recherches associées - extraction de mots-clés à partir de l'image Pins similaires - correspondance par vision par ordinateur Pins shopping - intégration produit Badge de prix - afficher le prix sur les pins Infos vendeur - détails du marchand Bouton Acheter - lien d'achat direct État du stock - indicateur de disponibilité
Tu disposes maintenant d'un clone complet de Pinterest avec grille en maçonnerie, tableaux, recherche visuelle et shopping !
Félicitations ! Tu as terminé les 14 modules de Vibecoding !
À bientôt !
Code de cette leçon : App.tsx
1'use client';
2
3import { useState } from 'react';
4
5export default function CreateBoardModal() {
6 const [isOpen, setIsOpen] = useState(true);
7 const [name, setName] = useState('');
8 const [isPrivate, setIsPrivate] = useState(false);
9
10 const handleSubmit = (e: React.FormEvent) => {
11 e.preventDefault();
12 if (name.trim()) {
13 alert(`Tableau créé : "${name}" (${isPrivate ? 'Secret' : 'Public'})`);
14 setIsOpen(false);
15 }
16 };
17
18 if (!isOpen) {
19 return (
20 <div className="min-h-screen bg-gray-100 flex items-center justify-center">
21 <button
22 onClick={() => setIsOpen(true)}
23 className="px-6 py-3 bg-red-600 text-white rounded-full font-semibold hover:bg-red-700"
24 >
25 Créer un tableau
26 </button>
27 </div>
28 );
29 }
30
31 return (
32 <div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
33 <div className="bg-white rounded-2xl max-w-md w-full p-6">
34 <h2 className="text-2xl font-bold mb-6">Créer un tableau</h2>
35
36 <form onSubmit={handleSubmit}>
37 <div className="mb-4">
38 <label className="block text-sm font-semibold mb-2">Nom</label>
39 <input
40 type="text"
41 value={name}
42 onChange={(e) => setName(e.target.value)}
43 placeholder='Comme "Voyages" ou "Recettes"'
44 className="w-full px-4 py-3 border-2 border-gray-200 rounded-2xl outline-none focus:border-blue-500"
45 autoFocus
46 />
47 </div>
48
49 <div className="mb-6">
50 <label className="flex items-start gap-3 cursor-pointer">
51 <input
52 type="checkbox"
53 checked={isPrivate}
54 onChange={(e) => setIsPrivate(e.target.checked)}
55 className="w-5 h-5 mt-1"
56 />
57 <div>
58 <div className="font-semibold">Garder ce tableau secret</div>
59 <div className="text-sm text-gray-600">Toi seul peux voir ce tableau</div>
60 </div>
61 </label>
62 </div>
63
64 <div className="flex gap-3">
65 <button
66 type="button"
67 onClick={() => setIsOpen(false)}
68 className="flex-1 py-3 border-2 border-gray-200 rounded-full font-semibold hover:border-gray-300"
69 >
70 Annuler
71 </button>
72 <button
73 type="submit"
74 disabled={!name.trim()}
75 className="flex-1 py-3 bg-red-600 text-white rounded-full font-semibold hover:bg-red-700 disabled:bg-gray-300"
76 >
77 Créer
78 </button>
79 </div>
80 </form>
81 </div>
82 </div>
83 );
84}Tu as repéré une erreur dans cette leçon ?
Teste tes connaissances
Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.
1. Comment fonctionne la recherche visuelle sur Pinterest ?
2. Que contient l'objet VisualSearchResult renvoyé par visualSearch ?
Voici 2 des 9 questions de cette leçon. Réponds aux autres dans le jeu.
Tâches pratiques dans le jeu
- Tri vertical
Ordonne les étapes de CameraSearch après l'import d'une image :
- Tri horizontal
Ordonne les éléments d'une ligne de la déclaration du prix dans l'interface Product :
- Clique dans l'ordre
Clique sur les éléments pour construire la lecture de l'image importée en Data URL :
- Éditeur de code
Crée une page de tableau
- Tri vertical
Ordonne les éléments de ShoppingPinCard de haut en bas :
- Clique dans l'ordre
Clique sur les éléments pour construire le formatage de la note du produit à une décimale :
- Tri horizontal
Ordonne les éléments d'une ligne du badge de prix sur une carte d'épingle shopping :
- Éditeur de code
Crée une interface de recherche visuelle
- Tri vertical
Ordonne le parcours complet de la recherche visuelle, de l'image jusqu'à l'achat :