Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

Boards i Pin Saving

Pinterest to organizacja przez boards - collections of pins.

Board Management

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 bg-opacity-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">Create board</h2>
24
25        <form onSubmit={handleSubmit}>
26          <div className="mb-4">
27            <label className="block text-sm font-semibold mb-2">Name</label>
28            <input
29              type="text"
30              value={name}
31              onChange={(e) => setName(e.target.value)}
32              placeholder="Np. 'Miejsca do odwiedzenia' lub 'Przepisy do zrobienia'"
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">Keep this board secret</div>
48                <div className="text-sm text-gray-600">
49                  Only you can see this board
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              Cancel
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              Create
69            </button>
70          </div>
71        </form>
72      </div>
73    </div>
74  );
75}

Save Pin to Board

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 bg-opacity-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">Save to board</h2>
27            <button
28              onClick={onClose}
29              className="text-gray-500 hover:text-black"
30            >
3132            </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">Create board</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            Save
108          </button>
109        </div>
110      </div>
111    </div>
112  );
113}

Board Page

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} collaborators</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                Edit board
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                Delete
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              All 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}

Podsumowanie 🎓

Create board - modal z name i privacy option ✅ Save pin - select board z preview ✅ Board page - pins organized in grid ✅ Board sections - organize pins within board ✅ Collaborators - multiple users can add pins ✅ Private boards - secret collections ✅ Edit/delete - board management ✅ Cover pin - main image for board

W następnym ćwiczeniu dodamy visual search i shopping!

Do zobaczenia! 🚀

Przejdź do CodeWorlds