Cours Vibe coding avec l'IA · Module 14: Clone de Pinterest
Tableaux et enregistrement de pins
Dans cette leçon4
Pinterest, c'est avant tout l'organisation via des tableaux - des collections de pins.
Gestion des tableaux
1'use client';
2
3interface CreateBoardModalProps {
4 onClose: () => void;
5 onCreate: (name: string, isPrivate: boolean) => void;
6}
7
8export default function CreateBoardModal({ onClose, onCreate }: CreateBoardModalProps) {
9 const [name, setName] = useState('');
10 const [isPrivate, setIsPrivate] = useState(false);
11
12 const handleSubmit = (e: React.FormEvent) => {
13 e.preventDefault();
14 if (name.trim()) {
15 onCreate(name, isPrivate);
16 onClose();
17 }
18 };
19
20 return (
21 <div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
22 <div className="bg-white rounded-2xl max-w-md w-full p-6">
23 <h2 className="text-2xl font-bold mb-6">Créer un tableau</h2>
24
25 <form onSubmit={handleSubmit}>
26 <div className="mb-4">
27 <label className="block text-sm font-semibold mb-2">Nom</label>
28 <input
29 type="text"
30 value={name}
31 onChange={(e) => setName(e.target.value)}
32 placeholder="Comme « Endroits à visiter » ou « Recettes à faire »"
33 className="w-full px-4 py-3 border-2 border-gray-200 rounded-2xl outline-none focus:border-blue-500"
34 autoFocus
35 />
36 </div>
37
38 <div className="mb-6">
39 <label className="flex items-center gap-3 cursor-pointer">
40 <input
41 type="checkbox"
42 checked={isPrivate}
43 onChange={(e) => setIsPrivate(e.target.checked)}
44 className="w-5 h-5"
45 />
46 <div>
47 <div className="font-semibold">Garder ce tableau secret</div>
48 <div className="text-sm text-gray-600">
49 Toi seul peux voir ce tableau
50 </div>
51 </div>
52 </label>
53 </div>
54
55 <div className="flex gap-3">
56 <button
57 type="button"
58 onClick={onClose}
59 className="flex-1 py-3 border-2 border-gray-200 rounded-full font-semibold hover:border-gray-300"
60 >
61 Annuler
62 </button>
63 <button
64 type="submit"
65 disabled={!name.trim()}
66 className="flex-1 py-3 bg-red-600 text-white rounded-full font-semibold hover:bg-red-700 disabled:bg-gray-300"
67 >
68 Créer
69 </button>
70 </div>
71 </form>
72 </div>
73 </div>
74 );
75}Enregistrer un pin dans un tableau
1'use client';
2
3interface SavePinModalProps {
4 pin: Pin;
5 boards: Board[];
6 onClose: () => void;
7 onSave: (boardId: string) => void;
8 onCreateBoard: () => void;
9}
10
11export default function SavePinModal({
12 pin,
13 boards,
14 onClose,
15 onSave,
16 onCreateBoard
17}: SavePinModalProps) {
18 const [selectedBoardId, setSelectedBoardId] = useState<string | null>(null);
19
20 return (
21 <div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
22 <div className="bg-white rounded-2xl max-w-md w-full max-h-[80vh] flex flex-col">
23 {/* Header */}
24 <div className="p-6 border-b border-gray-200">
25 <div className="flex items-center justify-between">
26 <h2 className="text-xl font-bold">Enregistrer dans un tableau</h2>
27 <button
28 onClick={onClose}
29 className="text-gray-500 hover:text-black"
30 >
31 ×
32 </button>
33 </div>
34 </div>
35
36 {/* Pin Preview */}
37 <div className="p-6 border-b border-gray-200">
38 <div className="flex gap-3">
39 <img
40 src={pin.imageUrl}
41 alt={pin.title}
42 className="w-16 h-16 rounded object-cover"
43 />
44 <div className="flex-1">
45 <h3 className="font-semibold line-clamp-1">{pin.title}</h3>
46 <p className="text-sm text-gray-600 line-clamp-2">
47 {pin.description}
48 </p>
49 </div>
50 </div>
51 </div>
52
53 {/* Boards List */}
54 <div className="flex-1 overflow-y-auto p-6">
55 <button
56 onClick={onCreateBoard}
57 className="w-full p-4 border-2 border-dashed border-gray-300 rounded-2xl hover:border-gray-400 mb-4"
58 >
59 <div className="flex items-center gap-3">
60 <div className="w-12 h-12 bg-gray-200 rounded flex items-center justify-center text-2xl">
61 +
62 </div>
63 <span className="font-semibold">Créer un tableau</span>
64 </div>
65 </button>
66
67 {boards.map(board => (
68 <button
69 key={board.id}
70 onClick={() => setSelectedBoardId(board.id)}
71 className={`
72 w-full p-4 rounded-2xl hover:bg-gray-100 mb-2
73 ${selectedBoardId === board.id ? 'bg-gray-100 ring-2 ring-blue-500' : ''}
74 `}
75 >
76 <div className="flex items-center gap-3">
77 {board.coverPinId ? (
78 <img
79 src={board.pins[0]?.imageUrl}
80 alt={board.name}
81 className="w-12 h-12 rounded object-cover"
82 />
83 ) : (
84 <div className="w-12 h-12 bg-gray-200 rounded" />
85 )}
86 <div className="flex-1 text-left">
87 <div className="font-semibold">{board.name}</div>
88 <div className="text-sm text-gray-600">
89 {board.pins.length} pins
90 </div>
91 </div>
92 {selectedBoardId === board.id && (
93 <div className="text-blue-500">√</div>
94 )}
95 </div>
96 </button>
97 ))}
98 </div>
99
100 {/* Save Button */}
101 <div className="p-6 border-t border-gray-200">
102 <button
103 onClick={() => selectedBoardId && onSave(selectedBoardId)}
104 disabled={!selectedBoardId}
105 className="w-full py-3 bg-red-600 text-white rounded-full font-semibold hover:bg-red-700 disabled:bg-gray-300"
106 >
107 Enregistrer
108 </button>
109 </div>
110 </div>
111 </div>
112 );
113}Page de tableau
1'use client';
2
3interface BoardPageProps {
4 board: Board;
5 isOwner: boolean;
6 onEditBoard: () => void;
7 onDeleteBoard: () => void;
8}
9
10export default function BoardPage({
11 board,
12 isOwner,
13 onEditBoard,
14 onDeleteBoard
15}: BoardPageProps) {
16 const [activeSection, setActiveSection] = useState<string | 'all'>('all');
17
18 return (
19 <div className="min-h-screen bg-white">
20 <div className="max-w-6xl mx-auto px-8 py-12">
21 {/* Header */}
22 <div className="text-center mb-8">
23 <h1 className="text-5xl font-bold mb-4">{board.name}</h1>
24 {board.description && (
25 <p className="text-xl text-gray-600 mb-4">{board.description}</p>
26 )}
27
28 <div className="flex items-center justify-center gap-4 text-sm text-gray-600 mb-6">
29 <span>{board.pins.length} pins</span>
30 {board.collaborators && board.collaborators.length > 0 && (
31 <>
32 <span>•</span>
33 <span>{board.collaborators.length} collaborateurs</span>
34 </>
35 )}
36 </div>
37
38 {isOwner && (
39 <div className="flex items-center justify-center gap-3">
40 <button
41 onClick={onEditBoard}
42 className="px-6 py-2 bg-gray-200 rounded-full font-semibold hover:bg-gray-300"
43 >
44 Modifier le tableau
45 </button>
46 <button
47 onClick={onDeleteBoard}
48 className="px-6 py-2 bg-gray-200 rounded-full font-semibold hover:bg-gray-300"
49 >
50 Supprimer
51 </button>
52 </div>
53 )}
54 </div>
55
56 {/* Sections */}
57 {board.sections && board.sections.length > 0 && (
58 <div className="flex gap-2 mb-8 overflow-x-auto">
59 <button
60 onClick={() => setActiveSection('all')}
61 className={`
62 px-4 py-2 rounded-full font-semibold whitespace-nowrap
63 ${activeSection === 'all'
64 ? 'bg-black text-white'
65 : 'bg-gray-200 hover:bg-gray-300'
66 }
67 `}
68 >
69 Tous les pins
70 </button>
71 {board.sections.map(section => (
72 <button
73 key={section.id}
74 onClick={() => setActiveSection(section.id)}
75 className={`
76 px-4 py-2 rounded-full font-semibold whitespace-nowrap
77 ${activeSection === section.id
78 ? 'bg-black text-white'
79 : 'bg-gray-200 hover:bg-gray-300'
80 }
81 `}
82 >
83 {section.name} ({section.pins.length})
84 </button>
85 ))}
86 </div>
87 )}
88
89 {/* Pins Grid */}
90 <MasonryGrid
91 pins={
92 activeSection === 'all'
93 ? board.pins
94 : board.sections?.find(s => s.id === activeSection)?.pins || []
95 }
96 />
97 </div>
98 </div>
99 );
100}Résumé
Créer un tableau - modale avec nom et option de confidentialité Enregistrer un pin - choisir un tableau avec aperçu Page de tableau - pins organisés en grille Sections de tableau - organiser les pins dans le tableau Collaborateurs - plusieurs utilisateurs peuvent ajouter des pins Tableaux privés - collections secrètes Modifier/supprimer - gestion des tableaux Pin de couverture - image principale du tableau
Dans le prochain exercice, nous ajouterons la recherche visuelle et le shopping !
À bientôt !
Code de cette leçon : App.tsx
1'use client';
2
3import { useState } from 'react';
4
5interface Pin {
6 id: string;
7 imageUrl: string;
8 title: string;
9 dominantColor: string;
10}
11
12const mockPin: Pin = {
13 id: '1',
14 imageUrl: 'https://images.unsplash.com/photo-1544551763-46a013bb70d5?w=400&h=600&fit=crop',
15 title: 'Magnifique coucher de soleil sur l\'océan',
16 dominantColor: '#e07856'
17};
18
19export default function PinCard() {
20 const [isHovered, setIsHovered] = useState(false);
21 const [imageLoaded, setImageLoaded] = useState(false);
22
23 return (
24 <div className="min-h-screen bg-gray-100 flex items-center justify-center p-8">
25 <div className="w-80">
26 <div
27 className="relative rounded-2xl overflow-hidden cursor-zoom-in group shadow-lg"
28 onMouseEnter={() => setIsHovered(true)}
29 onMouseLeave={() => setIsHovered(false)}
30 >
31 {/* Placeholder */}
32 {!imageLoaded && (
33 <div
34 className="w-full aspect-[2/3]"
35 style={{ backgroundColor: mockPin.dominantColor }}
36 />
37 )}
38
39 {/* Image */}
40 <img
41 src={mockPin.imageUrl}
42 alt={mockPin.title}
43 onLoad={() => setImageLoaded(true)}
44 className={`w-full h-auto block ${!imageLoaded ? 'absolute inset-0 object-cover' : ''}`}
45 />
46
47 {/* Hover Overlay */}
48 <div
49 className={`
50 absolute inset-0 bg-black/40 p-4 flex flex-col justify-between
51 transition-opacity duration-300
52 ${isHovered ? 'opacity-100' : 'opacity-0'}
53 `}
54 >
55 {/* Save Button */}
56 <div className="flex justify-end">
57 <button className="px-6 py-2 bg-red-600 text-white rounded-full font-semibold hover:bg-red-700 transition">
58 Enregistrer
59 </button>
60 </div>
61
62 {/* Title */}
63 <div>
64 <h3 className="text-white font-bold text-lg">{mockPin.title}</h3>
65 </div>
66 </div>
67 </div>
68
69 <div className="mt-4 text-center">
70 <p className="text-sm text-gray-600">Survole le pin pour voir l'overlay !</p>
71 </div>
72 </div>
73 </div>
74 );
75}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. Que sont les tableaux sur Pinterest ?
2. Que fait l'option « Garder ce tableau secret » lors de la création d'un tableau ?
Voici 2 des 6 questions de cette leçon. Réponds aux autres dans le jeu.
Tâches pratiques dans le jeu
- Tri horizontal
Ordonne les étapes de création d'un tableau :
- Clique dans l'ordre
Clique sur les éléments pour construire la déclaration de l'état de confidentialité dans CreateBoardModal :
- Éditeur de code
Crée une modale pour créer un tableau
- Tri vertical
Ordonne les étapes d'enregistrement d'une épingle via SavePinModal :
- Éditeur de code
Crée une modale pour enregistrer une épingle
- Tri vertical
Ordonne les sections de la page de tableau (BoardPage) de haut en bas :
- Clique dans l'ordre
Clique sur les éléments pour construire le gestionnaire de changement de section sur BoardPage :
- Tri horizontal
Ordonne les éléments sur une seule ligne de la case à cocher pour la confidentialité du tableau :