Cours Vibe coding avec l'IA · Module 14: Clone de Pinterest

Recherche visuelle et intégration du shopping

4 min de lecture
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. 1. Comment fonctionne la recherche visuelle sur Pinterest ?

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

Articles utiles