const LS_KEY = "pinboard.v1"; function saveToStorage(data) { try { localStorage.setItem(LS_KEY, JSON.stringify(data)); } catch (e) { console.error("Save error", e); } } function loadFromStorage() { try { const raw = localStorage.getItem(LS_KEY); return raw ? JSON.parse(raw) : null; } catch (e) { console.error("Load error", e); return null; } } function uid(prefix = "id") { return `${prefix}_${Math.random().toString(36).slice(2, 9)}_${Date.now()}`; } function fileToDataUrl(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(reader.result); reader.onerror = reject; reader.readAsDataURL(file); }); } // ---------- Main App ---------- export default function App() { const [boards, setBoards] = useState(() => { const saved = loadFromStorage(); return saved?.boards || [{ id: "default", name: "All Pins" }]; }); const [pins, setPins] = useState(() => loadFromStorage()?.pins || []); const [activeBoard, setActiveBoard] = useState("default"); const [query, setQuery] = useState(""); const [tagFilter, setTagFilter] = useState(""); const [modalPin, setModalPin] = useState(null); const [showAddPin, setShowAddPin] = useState(false); const [showNewBoard, setShowNewBoard] = useState(false); // Persist useEffect(() => { saveToStorage({ boards, pins }); }, [boards, pins]); const allTags = useMemo(() => { const s = new Set(); pins.forEach((p) => p.tags?.forEach((t) => s.add(t))); return Array.from(s).sort(); }, [pins]); const filteredPins = useMemo(() => { return pins .filter((p) => (activeBoard === "default" ? true : p.boardId === activeBoard)) .filter((p) => query ? (p.title + " " + (p.notes || "") + " " + (p.tags || []).join(" ")) .toLowerCase() .includes(query.toLowerCase()) : true ) .filter((p) => (tagFilter ? p.tags?.includes(tagFilter) : true)) .sort((a, b) => b.createdAt - a.createdAt); }, [pins, activeBoard, query, tagFilter]); function addBoard(name) { const newBoard = { id: uid("board"), name: name.trim() || "Untitled" }; setBoards((b) => [...b, newBoard]); setActiveBoard(newBoard.id); setShowNewBoard(false); } async function addPinsFromFiles(files, meta = {}) { const list = Array.from(files).filter((f) => f.type.startsWith("image/")); const newItems = []; for (const file of list) { const dataUrl = await fileToDataUrl(file); const pin = { id: uid("pin"), title: meta.title || file.name.replace(/\.[^.]+$/, ""), notes: meta.notes || "", tags: meta.tags || [], boardId: meta.boardId || activeBoard || "default", image: dataUrl, // Data URL (base64) createdAt: Date.now(), }; newItems.push(pin); } setPins((prev) => [...newItems, ...prev]); setShowAddPin(false); } function deletePin(id) { setPins((prev) => prev.filter((p) => p.id !== id)); setModalPin(null); } function updatePin(updated) { setPins((prev) => prev.map((p) => (p.id === updated.id ? updated : p))); } function movePinToBoard(pin, boardId) { updatePin({ ...pin, boardId }); } // Drag & drop uploader const dropRef = useRef(null); useEffect(() => { const el = dropRef.current; if (!el) return; const prevent = (e) => { e.preventDefault(); e.stopPropagation(); }; const onDrop = async (e) => { prevent(e); if (e.dataTransfer.files?.length) { await addPinsFromFiles(e.dataTransfer.files, {}); } }; ["dragenter", "dragover", "dragleave", "drop"].forEach((ev) => el.addEventListener(ev, prevent)); el.addEventListener("drop", onDrop); return () => { ["dragenter", "dragover", "dragleave", "drop"].forEach((ev) => el.removeEventListener(ev, prevent)); el.removeEventListener("drop", onDrop); }; }, []); return (
{/* Header */}
setQuery(e.target.value)} className="flex-1 rounded-xl border border-stone-300 px-4 py-2 focus:outline-none focus:ring-2 focus:ring-emerald-400" />
{/* Toolbar */}
Filter tag {!!(query || tagFilter) && ( )}
{/* Drop zone */}
Drag & drop images here to add pins
{/* Masonry Grid */}
{filteredPins.length === 0 ? ( setShowAddPin(true)} /> ) : ( {filteredPins.map((pin) => ( setModalPin(pin)} onMove={(b) => movePinToBoard(pin, b)} /> ))} )}
{/* Modals */} {showAddPin && ( setShowAddPin(false)} onAdd={addPinsFromFiles} /> )} {showNewBoard && ( setShowNewBoard(false)} onCreate={addBoard} /> )} {modalPin && ( setModalPin(null)} onDelete={() => deletePin(modalPin.id)} onSave={updatePin} /> )} {/* Styles */}
); } // ---------- UI Bits ---------- function Logo() { return (
Pinboard
); } function BoardTabs({ boards, active, onChange }) { return (
{boards.map((b) => ( ))}
); } function MasonryGrid({ children }) { return
{children}
; } function PinCard({ pin, boards, onOpen, onMove }) { const [hover, setHover] = useState(false); return (
{pin.title} setHover(true)} onMouseLeave={() => setHover(false)} onClick={onOpen} />
{pin.title}
{pin.tags?.length ? (
{pin.tags.map((t) => ( #{t} ))}
) : null}
); } function EmptyState({ onAdd }) { return (

No pins yet. Add a few images to get started.

); } function ModalShell({ children, onClose }) { useEffect(() => { const onKey = (e) => e.key === "Escape" && onClose(); window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [onClose]); return (
e.stopPropagation()}>
{children}
); } function NewBoardModal({ onClose, onCreate }) { const [name, setName] = useState(""); return (

New Board

setName(e.target.value)} />
); } function AddPinModal({ onClose, onAdd, boards, defaultBoard }) { const [files, setFiles] = useState([]); const [title, setTitle] = useState(""); const [notes, setNotes] = useState(""); const [tags, setTags] = useState(""); const [boardId, setBoardId] = useState(defaultBoard || "default"); const inputRef = useRef(null); return (

Add Pins

Drag and drop images here, or

setFiles(Array.from(e.target.files || []))} /> {files.length > 0 && (
{files.length} file(s) selected
)}
setTitle(e.target.value)} /> setTags(e.target.value)} />