// app/@modal/(.)photos/[id]/page.tsx
'use client';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
export default function PhotoModal({ params }: { params: { id: string } }) {
const router = useRouter();
const [isOpen, setIsOpen] = useState(true);
const handleClose = () => {
setIsOpen(false);
router.back();
};
return (
<div className="fixed inset-0 bg-black/70 flex items-center justify-center z-50" onClick={handleClose}>
<div className="bg-zinc-900 p-6 rounded-lg max-w-2xl w-full" onClick={(e) => e.stopPropagation()}>
<button onClick={handleClose} className="text-white mb-4 underline">Închide</button>
<div className="aspect-video w-full bg-zinc-800 flex items-center justify-center text-white">
Imaginea cu ID-ul: {params.id}
</div>
</div>
</div>
);
}Am implementat recent chestia asta la un portofoliu de fotografie cu vreo 12.000 de imagini indexate. Clientul voia neapărat ca atunci când dai click pe o poză din feed să se deschidă un modal rapid, fără să piardă contextul paginii din spate. În plus, dacă cineva dă share la link pe social media, trebuia să se încarce pagina de detaliu full-screen.
Să faci asta ca lumea în Next.js App Router e destul de complicat dacă nu înțelegi exact cum se pupă parallel routes cu intercepting routes.
Structura de foldere (Aici se rupe filmul de obicei)
Înainte să scrii o linie de cod, trebuie să organizezi corect directoarele. Altfel, Next.js pur și simplu ignoră rutele sau îți dă un ecran alb de te saturi de debug. Structura pe care am folosit-o noi arată așa:
app/layout.tsx(aici randăm@modalșichildren)page.tsx(feed-ul de poze)photos/[id]/page.tsx(pagina normală, full screen)
@modal/(.)photos/[id]/page.tsx(modalul care interceptează)
default.tsx(foarte important!)
Paranteza aia (.) înseamnă "interceptează ruta la același nivel de segment". E un fel de relative path pentru URL-uri. Când utilizatorul dă click pe un link spre /photos/12, routerul interceptează navigarea și randează folderul (.)photos/[id] în interiorul slotului @modal din layout, în loc să te trimită pe pagina full-screen.
De ce este default.tsx critic?
La prima încercare am omis default.tsx din root-ul @modal. Totul mergea perfect în local la click, dar când dădeam refresh pe pagina de feed, boom: 404 direct.
Next.js are nevoie de un fallback pentru slotul paralel atunci când ruta curentă nu se potrivește cu nimic din ce este definit în interiorul acelui slot. Fiindcă pe / nu avem niciun modal deschis, Next.js trebuie să știe ce să pună în @modal. Soluția e simplă: creezi un fișier default.tsx în @modal care returnează pur și simplu null.
Cum închizi modalul corect
Pentru a închide modalul și a reveni la URL-ul anterior, folosim routerul nativ din Next.js. Când utilizatorul dă click pe overlay sau pe butonul de închidere, apelăm router.back(). URL-ul revine la / și slotul @modal se golește automat.
Trade-off-uri de care trebuie să știi
- Avantaj: Am redus bounce rate-ul cu aproape 15% pentru că utilizatorii pot naviga prin zeci de poze fără să piardă poziția de scroll din feed. În plus, avem SEO perfect pentru că fiecare poză are un URL unic accesibil direct.
- Dezavantaj: E incredibil de greu de debuguit când ceva nu se potrivește în structura de foldere. Next.js e extrem de sensibil la denumiri și uneori trebuie să ștergi manual folderul
.nextși să repornești serverul ca să își dea seama de noile rute paralele. - Animații: Dacă folosești Framer Motion pentru animații de ieșire (exit animations), e destul de dificil să le sincronizezi cu
router.back(), deoarece Next.js va demonta componenta instant.
Voi ce folosiți pentru modalele de detalii? Rămâneți pe clasicul query params de tipul ?photoId=12 sau ați trecut deja pe structura asta nativă din App Router?