/* 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}
{ops.length ? "Opinie · " + ops.length : "Brak opinii — bądź pierwszy"}
{ops.map((o) =>
)}
);
}
function FeedbackPage() {
const store = useStore();
const user = store.currentUser();
const projects = store.state.projects || [];
return (
Opinie
{user.isAdmin
? "Wrzuć zdjęcia projektów — zespół doda swoje opinie"
: "Przeglądaj projekty i dodawaj opinie · projekty dodaje administrator"}
{user.isAdmin ?
: null}
{projects.length
? projects.map((p) =>
)
:
Brak projektów do opiniowania.{user.isAdmin ? " Przeciągnij zdjęcia powyżej." : " Administrator jeszcze nic nie dodał."}
}
);
}
Object.assign(window, { FeedbackPage });