Pinterest to organizacja przez boards - collections of pins.
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}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 >
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">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}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}✅ 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! 🚀