I am raw html block.
Click edit button to change timport React, { useEffect, useMemo, useRef, useState } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Switch } from "@/components/ui/switch"; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; import { Trash2, Save, Upload, Download, ZoomIn, ZoomOut, Move, Undo2, Redo2, ImageUp, Import, FileDown } from "lucide-react"; import html2canvas from "html2canvas"; // Typy type MarkerStatus = "zajęte" | "wolne"; type Marker = { id: string; xPct: number; // 0..100 w % szerokości obrazu yPct: number; // 0..100 w % wysokości obrazu etykieta?: string; // np. numer miejsca z obrazka status: MarkerStatus; }; // Pomoc – proste ID const uid = () => Math.random().toString(36).slice(2, 10); export default function PlanSaliApp() { // Obraz tła (plan widowni) const [imgSrc, setImgSrc] = useState(null); // Markery (miejsca oznaczone na planie) const [markers, setMarkers] = useState([]); // UI const [wpisujEtykietePoKliknieciu, setWpisujEtykietePoKliknieciu] = useState(true); const [pokazNumery, setPokazNumery] = useState(true); // Zoom / Pan const [zoom, setZoom] = useState(1); const [offset, setOffset] = useState({ x: 0, y: 0 }); const dragging = useRef(false); const lastPos = useRef({ x: 0, y: 0 }); // Cofnij / Powtórz const [undoStack, setUndoStack] = useState([]); const [redoStack, setRedoStack] = useState([]); // Refy const stageRef = useRef(null); const contentRef = useRef(null); // Autosave w localStorage useEffect(() => { const saved = localStorage.getItem("planSali_state_v1"); if (saved) { try { const parsed = JSON.parse(saved); if (parsed.imgSrc) setImgSrc(parsed.imgSrc); if (parsed.markers) setMarkers(parsed.markers); } catch {} } }, []); useEffect(() => { localStorage.setItem( "planSali_state_v1", JSON.stringify({ imgSrc, markers }) ); }, [imgSrc, markers]); // Zapisywanie stanu do stosu (do cofania) const snapshot = () => JSON.stringify({ markers }); const pushUndo = () => setUndoStack((s) => [...s, snapshot()]); const applySnapshot = (snap: string) => { try { const parsed = JSON.parse(snap) as { markers: Marker[] }; setMarkers(parsed.markers || []); } catch {} }; const cofnij = () => { if (!undoStack.length) return; const prev = undoStack[undoStack.length - 1]; setUndoStack((s) => s.slice(0, -1)); setRedoStack((s) => [...s, snapshot()]); applySnapshot(prev); }; const powtorz = () => { if (!redoStack.length) return; const next = redoStack[redoStack.length - 1]; setRedoStack((s) => s.slice(0, -1)); setUndoStack((s) => [...s, snapshot()]); applySnapshot(next); }; // Upload obrazu const onUpload = (file: File) => { const reader = new FileReader(); reader.onload = () => setImgSrc(reader.result as string); reader.readAsDataURL(file); }; // Oblicz pozycję kliknięcia w % względem obrazu (po uwzględnieniu zoom/pan) const clientToPercent = (e: React.MouseEvent) => { if (!contentRef.current || !stageRef.current) return { xPct: 0, yPct: 0 }; const stageRect = stageRef.current.getBoundingClientRect(); const content = contentRef.current; const cx = (e.clientX - stageRect.left - offset.x) / zoom; const cy = (e.clientY - stageRect.top - offset.y) / zoom; const w = content.scrollWidth; // natural width po transformacji == baza const h = content.scrollHeight; const xPct = (cx / w) * 100; const yPct = (cy / h) * 100; return { xPct, yPct }; }; // Klik na scenie – dodaj / przełącz marker const handleStageClick = (e: React.MouseEvent) => { // Pomiń przeciąganie if ((e.target as HTMLElement).dataset.role === "marker") return; const { xPct, yPct } = clientToPercent(e); if (xPct < 0 || yPct < 0 || xPct > 100 || yPct > 100) return; pushUndo(); setRedoStack([]); let etykieta: string | undefined = undefined; if (wpisujEtykietePoKliknieciu) { // lekkie prompt – w UI nie blokuje etykieta = window.prompt("Numer miejsca / etykieta (opcjonalnie)") || undefined; } const nowy: Marker = { id: uid(), xPct, yPct, etykieta, status: "zajęte" }; setMarkers((m) => [...m, nowy]); }; const toggleStatus = (id: string) => { pushUndo(); setRedoStack([]); setMarkers((m) => m.map((mk) => mk.id === id ? { ...mk, status: mk.status === "zajęte" ? "wolne" : "zajęte" } : mk ) ); }; const deleteMarker = (id: string) => { pushUndo(); setRedoStack([]); setMarkers((m) => m.filter((mk) => mk.id !== id)); }; const editLabel = (id: string) => { const txt = window.prompt("Zmień etykietę (pozostaw puste aby usunąć)"); if (txt === null) return; pushUndo(); setRedoStack([]); setMarkers((m) => m.map((mk) => (mk.id === id ? { ...mk, etykieta: txt || undefined } : mk))); }; // Zoom/pan obsługa const onWheel = (e: React.WheelEvent) => { e.preventDefault(); const factor = e.deltaY > 0 ? 0.9 : 1.1; const newZoom = Math.min(4, Math.max(0.3, zoom * factor)); // Zoom względem kursora – oblicz przesunięcie tak, aby punkt pod kursorem został w miejscu if (stageRef.current) { const rect = stageRef.current.getBoundingClientRect(); const cx = e.clientX - rect.left; const cy = e.clientY - rect.top; const nx = cx - ((cx - offset.x) * newZoom) / zoom; const ny = cy - ((cy - offset.y) * newZoom) / zoom; setOffset({ x: nx, y: ny }); } setZoom(newZoom); }; const onMouseDown = (e: React.MouseEvent) => { if ((e.target as HTMLElement).dataset.role === "marker") return; dragging.current = true; lastPos.current = { x: e.clientX, y: e.clientY }; }; const onMouseMove = (e: React.MouseEvent) => { if (!dragging.current) return; const dx = e.clientX - lastPos.current.x; const dy = e.clientY - lastPos.current.y; lastPos.current = { x: e.clientX, y: e.clientY }; setOffset((o) => ({ x: o.x + dx, y: o.y + dy })); }; const onMouseUp = () => (dragging.current = false); const onMouseLeave = () => (dragging.current = false); // Eksport / Import const exportJSON = () => { const blob = new Blob([ JSON.stringify({ markers, imgSrc }, null, 2), ], { type: "application/json" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = "plan_sali_stan.json"; a.click(); URL.revokeObjectURL(url); }; const importJSON = (file: File) => { const reader = new FileReader(); reader.onload = () => { try { const parsed = JSON.parse(String(reader.result)); if (parsed.imgSrc) setImgSrc(parsed.imgSrc); if (parsed.markers) setMarkers(parsed.markers); } catch (e) { alert("Niepoprawny plik JSON"); } }; reader.readAsText(file); }; const exportPNG = async () => { if (!stageRef.current) return; const canvas = await html2canvas(stageRef.current, { backgroundColor: null }); const url = canvas.toDataURL("image/png"); const a = document.createElement("a"); a.href = url; a.download = "plan_sali_zaznaczone.png"; a.click(); }; const liczZajete = useMemo(() => markers.filter((m) => m.status === "zajęte").length, [markers]); return (

{/* Panel boczny */} Ustawienia
{ const f=e.target.files?.[0]; if (f) onUpload(f); }} />

Możesz załadować grafikę z numerami miejsc – np. tę z załącznika.

Zapytaj o etykietę przy dodawaniu
Pokazuj etykiety na markerach
Legenda:
zajęte wolne
Szybkie akcje: kliknij na planie, aby dodać marker (domyślnie „zajęte”). Klik na markerze przełącza status, Shift+klik usuwa, Alt+klik edytuje etykietę.
Zajęte: {liczZajete} / Wszystkie: {markers.length}
{/* Główna scena */} Plan sali – kliknij, aby oznaczać miejsca
{imgSrc ? ( Plan sali ) : (
Wczytaj grafikę planu sali, aby rozpocząć.
)} {/* Markery */} {imgSrc && markers.map((m) => (
{ e.stopPropagation(); if (e.nativeEvent instanceof MouseEvent && e.nativeEvent.shiftKey) return deleteMarker(m.id); if (e.nativeEvent instanceof MouseEvent && (e.nativeEvent.altKey || e.nativeEvent.metaKey)) return editLabel(m.id); toggleStatus(m.id); }} title={m.etykieta || "(bez etykiety)"} >
{pokazNumery && (m.etykieta?.slice(0,3) || "")}
))}
{/* Lista znaczników */}
Lista zaznaczonych miejsc
{markers.length === 0 ? (
Brak – kliknij na planie, aby dodać.
) : (
{markers.map((m)=> ( ))}
Etykieta Status Pozycja Akcje
{m.etykieta || (brak)} {m.status} {m.xPct.toFixed(1)}%, {m.yPct.toFixed(1)}%
)}
{/* Stopka */}
Dwuklik na planie dodaje marker. Przeciągaj, aby przesuwać. Rolka – powiększenie.
); } his html