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

208 lines
8.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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:
```typescript
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):**
```tsx
// 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:
```typescript
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
```typescript
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:
```typescript
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)