/* TargiApp — zakładka Opinie: admin wrzuca zdjęcia projektów (drag&drop), każdy zalogowany pisze opinie (osoba + treść). Czytanie: wszyscy. Dodawanie projektów: tylko admin. */ function fmtWhen(ts) { const d = new Date(ts); return d.toLocaleDateString("pl-PL", { day: "2-digit", month: "2-digit" }) + ", " + d.toLocaleTimeString("pl-PL", { hour: "2-digit", minute: "2-digit" }); } // zmniejsza zdjęcie do rozsądnego rozmiaru i zwraca dataURL (JPEG) function fileToDataUrl(file, max, quality) { return new Promise((resolve, reject) => { if (!file.type || !file.type.startsWith("image/")) return reject(new Error("nie obraz")); const img = new Image(); const url = URL.createObjectURL(file); img.onload = () => { let w = img.naturalWidth, h = img.naturalHeight; const s = Math.min(1, (max || 1600) / Math.max(w, h)); w = Math.round(w * s); h = Math.round(h * s); const c = document.createElement("canvas"); c.width = w; c.height = h; c.getContext("2d").drawImage(img, 0, 0, w, h); URL.revokeObjectURL(url); resolve(c.toDataURL("image/jpeg", quality || 0.82)); }; img.onerror = () => { URL.revokeObjectURL(url); reject(new Error("błąd wczytania")); }; img.src = url; }); } function ProjectDropzone() { const store = useStore(); const [over, setOver] = useState(false); const [busy, setBusy] = useState(false); const inputRef = useRef(null); async function handleFiles(fileList) { const files = [...fileList].filter((f) => f.type && f.type.startsWith("image/")); if (!files.length) return; setBusy(true); for (const f of files) { try { const data = await fileToDataUrl(f, 1600, 0.82); store.addProject(f.name.replace(/\.[^.]+$/, ""), data); } catch (e) { /* pomiń plik */ } } setBusy(false); } return (
{ e.preventDefault(); setOver(true); }} onDragLeave={() => setOver(false)} onDrop={(e) => { e.preventDefault(); setOver(false); handleFiles(e.dataTransfer.files); }} onClick={() => inputRef.current && inputRef.current.click()} > { handleFiles(e.target.files); e.target.value = ""; }} />
🖼️
{busy ? "Wczytywanie…" : "Przeciągnij zdjęcia projektów tutaj"}
albo kliknij, aby wybrać pliki · każde zdjęcie = osobny projekt do opiniowania
); } function OpinionRow({ o, canDelete }) { const store = useStore(); const p = store.personById(o.personId); return (
{store.fullName(p)} {fmtWhen(o.createdAt)} {canDelete ? : null}
{o.text}
); } function ProjectCard({ p }) { const store = useStore(); const user = store.currentUser(); const ops = store.opinionsForProject(p.id); const [text, setText] = useState(""); function submit() { if (store.addOpinion(p.id, text)) setText(""); } return (
{user.isAdmin ? store.updateProject(p.id, { title: e.target.value })} /> :
{p.title}
} {user.isAdmin ? : null}
{p.title}
{ops.length ? "Opinie · " + ops.length : "Brak opinii — bądź pierwszy"}
{ops.map((o) => )}