Files
ArchiTools/temp-preview-prompt.md
T
AI Assistant 09a24233bb feat(parcel-sync): full GPKG export workflow with UAT autocomplete, hero buttons, layer catalog
- Fix login button (return success instead of ok)
- Add UAT autocomplete with NFD-normalized search (3186 entries)
- Add export-bundle API: base mode (terenuri+cladiri) + magic mode (enriched parcels)
- Add export-layer-gpkg API: individual layer GPKG download
- Add gpkg-export service: ogr2ogr with GeoJSON fallback
- Add reproject service: EPSG:3844 projection support
- Add magic-mode methods to eterra-client (immApps, folosinte, immovableList, docs, parcelDetails)
- Rewrite UI: 3-tab layout (Export/Catalog/Search), progress tracking, phase trail
2026-03-06 06:53:49 +02:00

8.3 KiB
Raw Blame History

Prompt: Înlocuiește sistemul actual de preview documente atașate cu QuickLook-style Preview

Copiază tot conținutul de mai jos și dă-l ca instrucțiune directă modelului AI.


Înlocuiește complet sistemul actual de previzualizare a documentelor atașate cu un preview fullscreen inspirat de macOS Quick Look. Componentă React standalone, un singur fișier, fără dependențe externe — doar React hooks, lucide-react icons, Tailwind CSS, și un Button component din UI library.

Arhitectură

Un singur component exportat: AttachmentPreview

Props:

interface AttachmentPreviewProps {
  /** Array cu fișierele previewable (filtrate în prealabil) */
  attachments: Attachment[];
  /** Indexul inițial de afișat */
  initialIndex: number;
  /** Dacă modal-ul e deschis */
  open: boolean;
  /** Callback la închidere */
  onClose: () => void;
}

interface Attachment {
  id: string;
  name: string;
  data: string; // base64 data URI (data:image/jpeg;base64,... sau data:application/pdf;base64,...)
  type: string; // MIME type: "image/jpeg", "application/pdf", etc.
  size: number; // bytes
}

Pattern de integrare (în componenta părinte):

// Folosește key={previewIndex} ca să forțeze remount-ul la fiecare deschidere nouă.
// Asta resetează zoom, pan, currentIndex fără setState în effects.
{
  previewIndex !== null && (
    <AttachmentPreview
      key={previewIndex}
      attachments={previewableAttachments}
      initialIndex={previewIndex}
      open
      onClose={() => setPreviewIndex(null)}
    />
  );
}

Helper exportat: getPreviewableAttachments(attachments) Filtrează array-ul la atașamentele care pot fi previewate (au data non-empty, MIME type = image/* sau application/pdf).

Structura UI (3 zone fixe)

1. Top Bar (shrink-0)

  • Stânga: icon fișier + filename (truncate) + dimensiune KB + extensie + "N / M" counter (dacă sunt multiple fișiere)
  • Dreapta: [Zoom | 100% | Zoom+] (doar pentru imagini) | Print | Download | Close(X)
  • Separatoare vizuale între grupuri de butoane: div.w-px.h-5.bg-white/20.mx-1

2. Content Area (flex-1)

  • Container: fixed inset-0 z-[100], bg-black/90 backdrop-blur-sm
  • Imagini: <img> cu transform: translate(panX, panY) scale(zoom), transition-transform duration-150, draggable={false}, select-none
  • PDF-uri: <iframe src={blobUrl} className="w-full h-full border-0"> — folosește blob URL, NU data: URI direct
  • Fallback (tip nesuportat): icon mare + text + buton Download
  • Săgeți navigare: cercuri absolute left-3/right-3 top-1/2, cu e.stopPropagation() pe click

3. Bottom Thumbnails (shrink-0, doar când sunt multiple fișiere)

  • overflow-x-auto, gap-2
  • Imagini: <img className="h-10 w-14 object-cover rounded">
  • PDF-uri: div.h-10.w-14 cu FileText icon pe fundal bg-white/10
  • Selectat: border-white/80 ring-1 ring-white/30; neselectat: opacity-50 hover:opacity-80

Detalii tehnice critice

PDF rendering via Blob URL (NU data: URI)

Data URI-urile nu funcționează bine în iframe-uri. Convertește base64 la Blob:

const pdfBlobUrl = useMemo(() => {
  if (!isPdf || !att?.data) return null;
  try {
    const base64 = att.data.split(",")[1]; // strip "data:application/pdf;base64,"
    if (!base64) return null;
    const bytes = atob(base64);
    const arr = new Uint8Array(bytes.length);
    for (let i = 0; i < bytes.length; i++) arr[i] = bytes.charCodeAt(i);
    const blob = new Blob([arr], { type: "application/pdf" });
    return URL.createObjectURL(blob);
  } catch {
    return null;
  }
}, [isPdf, att?.data]);

// IMPORTANT: revocă blob URL la cleanup ca să nu existe memory leak
useEffect(() => {
  return () => {
    if (pdfBlobUrl) URL.revokeObjectURL(pdfBlobUrl);
  };
}, [pdfBlobUrl]);

Zoom (doar imagini)

  • Range: 25% — 500%, step 25%
  • Scroll wheel: onWheel cu e.preventDefault(), deltaY > 0 = zoom out
  • Keyboard: +/= zoom in, - zoom out, 0 reset la 100%
  • Click pe procentajul afișat resetează la 100%

Pan (doar imagini, doar când zoom > 1)

  • onMouseDown: salvează start position în useRef (nu state! — ref e mai performant pentru drag)
  • onMouseMove: calculează delta din ref
  • onMouseUp/onMouseLeave: stop panning
  • Cursor: cursor-grab când zoom > 1, cursor-grabbing când panning activ
  • isPanning e singura stare React; coordonatele start sunt în ref: useRef({ x: 0, y: 0, panX: 0, panY: 0 })

Print

  • PDF: creează iframe ascuns cu src=blobUrl, iframe.onload = () => iframe.contentWindow.print(), cleanup cu setTimeout(() => document.body.removeChild(iframe), 1000)
  • Imagine: window.open("", "_blank") cu HTML inline care conține <img>, w.onload = () => w.print()

Download

const a = document.createElement("a");
a.href = att.data; // data URI original
a.download = att.name;
a.click();

Keyboard handler (useEffect)

  • Escape → close
  • ArrowLeft/ArrowRight → navigate (circular: last→first, first→last)
  • +/=/-/0 → zoom controls
  • Cleanup obligatoriu: removeEventListener în return

React 19 compatibility (FOARTE IMPORTANT)

  • NU pune setState în useEffect — React 19 strict mode dă eroare la "Calling setState synchronously within an effect"
  • NU accesa ref.current direct în render body — tot eroare: "Cannot access refs during render"
  • Folosește key={index} pe componentă ca să forțezi remount cu state proaspăt la fiecare deschidere nouă
  • Zoom/pan reset la navigare: apelează resetView() direct în callback-ul de navigare, NU într-un effect separat:
const resetView = useCallback(() => {
  setZoom(1);
  setPan({ x: 0, y: 0 });
}, []);

// În keyboard handler și goNext/goPrev — resetView() inline:
setCurrentIndex((i) => {
  const next = i < attachments.length - 1 ? i + 1 : 0;
  resetView(); // apelat inline, nu în effect
  return next;
});

Styling complet

  • Container principal: fixed inset-0 z-[100] flex flex-col bg-black/90 backdrop-blur-sm animate-in fade-in duration-200
  • Top/bottom bars: bg-black/60 border-b/border-t border-white/10
  • Text pe dark background: text-white, text-white/70, text-white/50
  • Buttons: variant="ghost" cu text-white/70 hover:text-white hover:bg-white/10, size h-8 w-8
  • Zoom percentage text: font-mono min-w-[3.5rem] text-center text-xs
  • Navigation arrows: rounded-full bg-black/50 hover:bg-black/70 w-10 h-10, centered cu flex items-center justify-center
  • Imagine: select-none transition-transform duration-150, stil inline pentru transform
  • PDF iframe: w-full h-full border-0

Dependențe

  • React: useState, useEffect, useCallback, useMemo, useRef
  • lucide-react: ChevronLeft, ChevronRight, Download, FileText, Minus, Plus, Printer, X
  • Tailwind CSS (classes direct pe elemente)
  • Un Button component din UI library (cu props: variant="ghost", size="icon")
  • cn() utility pentru conditional classes (opțional, poți folosi template literals)

Ce NU trebuie să facă

  • NU depinde de Dialog/Radix — e pur fixed div (simplu, zero probleme cu z-index sau portal nesting)
  • NU stochează fișiere pe server — totul din data: URI-urile deja existente în state
  • NU face streaming/lazy load — fișierul e deja în memorie ca base64
  • NU suportă video/audio (doar image/* și application/pdf)
  • NU folosește window.open("file:///...") pentru fișiere locale — browserele blochează file:/// URLs din pagini web (security restriction)

Checklist final

  • Componenta e un singur fișier exportat
  • Funcționează cu imagini (zoom cu scroll wheel, pan cu drag, reset cu 0)
  • Funcționează cu PDF-uri (blob URL în iframe, nu data: URI)
  • Navigare cu săgeți (keyboard + butoane) când sunt multiple fișiere
  • Thumbnail strip în josul ecranului când sunt multiple
  • Download și Print funcționează pentru ambele tipuri
  • Escape închide preview-ul
  • Zero setState direct în useEffect (React 19 compatible)
  • Zero ref.current accesat în render body (React 19 compatible)
  • key={index} pe componentă pentru remount clean
  • URL.revokeObjectURL() apelat la cleanup pentru PDF blob URLs
  • e.stopPropagation() pe butoanele de navigare (să nu trigger pan/zoom)