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

Tableaux et enregistrement de pins

4 min de lecture
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. 1. Que sont les tableaux sur Pinterest ?

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

Articles utiles