Rotalar ve Kabuk
Rota tablosu
| URL | Ekran | Dosya | Bileşen türü |
|---|---|---|---|
/ | Tanıtım (landing) sayfası | app/(marketing)/page.tsx | Server |
/login | Oturum aç | app/login/page.tsx (+ login-form.tsx) | Server (form Client) |
/auth/confirm | Sihirli bağlantı dönüşü (route handler) | app/auth/confirm/route.ts | — |
/api/license/verify | Lisans doğrulama (route handler) | app/api/license/verify/route.ts | — |
/panel | Panel | app/(panel)/panel/page.tsx | Server |
/menu | Menü Yönetimi | app/(panel)/menu/page.tsx | Client |
/stock | Stok | app/(panel)/stock/page.tsx | Client |
/devices | Cihazlar | app/(panel)/devices/page.tsx | Client |
/apps | Uygulama Yönetimi | app/(panel)/apps/page.tsx | Server |
/settings | Restoran Ayarları | app/(panel)/settings/page.tsx | Client |
/staff | Personel & Roller | app/(panel)/staff/page.tsx | Client |
/sync | Senkronizasyon | app/(panel)/sync/page.tsx | Client |
/errors | Hata Kayıtları | app/(panel)/errors/page.tsx | Client |
/audit | Denetim Kaydı | app/(panel)/audit/page.tsx | Client |
/license | Lisans | app/(panel)/license/page.tsx | Client |
/fiscal | e-Belge · GİB | app/(panel)/fiscal/page.tsx | Client |
/billing | Abonelik | app/(panel)/billing/page.tsx | Client |
Yalnızca / build sırasında statik prerender edilir. (panel) rotaları ve /login oturum çerezini okuduğu için, /api/license/verify ile /auth/confirm ise route handler olduğu için istek başına çalışır. Bkz. Kimlik doğrulama.
(panel) rota grubu
Parantezli klasör URL’ye segment eklemez. Grup, kabuğu (kenar çubuğu, başlık ve
kayan içerik alanı) on üç kardeş rotaya tek bir layout.tsx ile bağlar. Böylece
/menu, /stock, /settings gibi rotalar URL’de panel/ öneki olmadan en üst
seviyede kalır. Bu, tasarımdaki düz kenar çubuğuyla örtüşür.
/login bilerek grubun dışında tutulur ve kabuk olmadan render edilir.
Dashboard / değil /panel adresindedir. Dashboard’a açık bir /panel segmenti
vermek her navigasyon hedefini gerçek ve paylaşılabilir bir URL yapar, kenar
çubuğundaki aktif satır tespitini basit bir pathname === href karşılaştırmasına
indirir ve /’yi tanıtım sayfasına bırakır.
(marketing) rota grubu: tanıtım sayfası
/ adresi, Respos’u restoran sahiplerine tanıtan herkese açık sayfadır
(app/(marketing)/page.tsx). Yönetim kabuğu olmadan, yalnızca kök layout ile
render edilir.
- Server Component; iki client adası var: animasyon ve geri sayım. SSS
<details>ile açılır, menü sayfa içi bağlantılardır, tüm çağrılar bağlantıdır. Bölümler sunucuda render edilir ve_components/landing-motion.tsx("use client") tarafındanchildrenolarak sarılır; bu ada kendi başına hiçbir şey çizmez, yalnızca işaretlemedekidata-*kancalarını GSAP (gsap+@gsap/react, ScrollTrigger ve ScrollToPlugin) ile canlandırır: hero girişi (kelime kelime başlık, önizleme çubuklarının yükselmesi), kaydırmayla beliren bölümler, sayaçlar, hücrelerde hover çizgisi, imleci izleyen CTA ve önizleme eğimi, yumuşak sayfa içi kaydırma. Gizleme CSS’te(scripting: enabled) and (prefers-reduced-motion: no-preference)arkasındadır; JavaScript kapalıysa ya da kullanıcı azaltılmış hareket istiyorsa sayfa animasyonsuz ve eksiksiz görünür. Hydration hiç gelmezse 3 sn sonra her şey CSS ile görünür olur.data-heroalan her öğe GSAP gösterene kadar gizli başlar. Bu yüzden yeni birdata-herodeğeri eklenirselanding-motion.tsxzaman çizelgesine de adım eklenmelidir; yoksa öğe hiç görünmez. - Lansman geri sayımı (SCRUM-201,
_components/launch-countdown.tsx,"use client") hero’da CTA düğmelerinin altındadır. İlk demo / beta sürümüne gün, saat, dakika ve saniye olarak kalan süreyi gösterir. Hedef tarih, etiket ve başlık_content.tsiçindekiLAUNCHsabitindedir (şu an2027-01-01T00:00:00+03:00, Türkiye saatiyle gece yarısı). Tarihi değiştirmek için yalnızca bu sabit düzenlenir. Sayfa build sırasında üretildiği için saatuseSyncExternalStoreile okunur: sunucu HTML’i––yer tutucularını ve sabit<time>etiketini taşır, canlı değer hydration sırasında uyuşmazlık olmadan gelir. Tarih geçince sayaç sıfırda kalır. Ziyaretçinin cihaz saatine dayanır. - Marka ve alan adı
app/_data/site.tsiçindedir:BRAND(“Respos”),BRAND_WORDMARK(“RESPOS”),SITE_DOMAIN(respos.com.tr),SITE_URL,CONTACT_EMAIL(iletisim@respos.com.tr) veCOMPANY(“Overfit Soft”). Kenar çubuğu, giriş ekranı, tanıtım sayfasının başlığı ve alt bilgisi ilemetadatahep buradan okur; ürün adı başka hiçbir yerde sabit yazılmaz. Kök layoutmetadataBase’iSITE_URLyapar, tanıtım sayfasıcanonicalve Open Graph URL’sini/olarak verir; ikisi birliktehttps://respos.com.tr/üretir. Ekran metinlerindeki küçük harfli “adisyon” ürün adı değil, masanın hesabı anlamındaki sözcüktür; olduğu gibi kalır. - Metinler
_content.tsdosyasındadır. İletişim adresisite.ts’ten yeniden dışa aktarılanCONTACT_EMAILsabitidir; “Demo talep et” ve plan düğmeleri bu adresemailto:açar. Form yok, hiçbir şey gönderilmez. - Önizleme (
_components/product-preview.tsx) dashboard’un küçük bir kopyasıdır. Görsel dosyası değil düzdiv’lerle çizilir ve fixture verisini “Örnek veri” etiketiyle gösterir. - Fiyatlar Abonelik ekranıyla aynı
PLANSverisinden okunur, bu yüzden iki ekran birbiriyle çelişemez. - Panelin aksine telefon genişliğinde (~390px) de düzgün görünecek şekilde tasarlanmıştır.
Kabuk
app/(panel)/layout.tsx bir Server Component’tir. Yerleşim, 248px kenar
çubuğu ile minmax(0,1fr) içerik sütunundan oluşan bir ızgaradır. İçerik sütunu
64px başlık ve kayan <main> alanını barındırır. Kabuk viewport yüksekliğine
kilitlidir: yalnızca <main> kayar, kenar çubuğu ve başlık sabit kalır.
İçindeki üç parça client adasıdır:
| Bileşen | Neden client |
|---|---|
_components/sidebar.tsx | Aktif satırı usePathname ile belirler |
_components/header.tsx | Ekran başlığını usePathname ile belirler, şube seçicisini barındırır |
_components/tenant-context.tsx | Seçili şubeyi tutar. Başlık değiştirir, kenar çubuğu gösterir; iki tüketicisi olduğu için ikisinden birinde yaşayamaz |
TenantProvider yalnızca bu iki adayı ve {children}’ı sarar, <html>’i sarmaz.
Ekran kaydı: _screens.ts
Navigasyon ve başlık metinleri tek bir kayıttan gelir. Her Screen şunları taşır:
href: rotalabel: kenar çubuğunda görünen ad (ör. “Menü”)titlevekicker: başlıkta görünen ad ve alt satır (ör. “Menü Yönetimi”)badge(isteğe bağlı): kenar çubuğu satırının sağındaki sayaçtone: "tag": dikkat gerektiren durumlar için accent renkli etikettone: "muted": bilgi amaçlı sayılar için gri metin
label ile title bilerek farklıdır; tasarımdaki SCREENS haritası da böyledir.
Başlık screenFor(pathname) ile bulunur, bu yüzden hiçbir sayfa kendi başlığını
ayrıca bağlamaz. Rotayla eşleşmeyen bir yol Panel başlığına düşer.
Rozet değerleri app/_data/panel.ts içindeki BADGES nesnesinden okunur. Bkz.
Veri ve entegrasyon.
Şube seçici
Başlıktaki seçici TENANTS fixture’ından üç şubeyi listeler. Seçim yalnızca
client state’tir: sayfa yenilenince ilk şubeye döner ve hiçbir ekranın verisini
filtrelemez. Gerçek çok kiracılı (multi-tenant) çözüm, SCRUM-37 altındaki
session/JWT tenant claim’ine (SCRUM-141) ve tenant bazlı RLS’e (SCRUM-140)
bağlıdır.
Yeni ekran eklemek
app/(panel)/<segment>/page.tsxoluşturun._screens.tsiçindekiSCREENSdizisine bir kayıt ekleyin. Kenar çubuğundaki sıra dizideki sıradır.- Rozet gerekiyorsa
BadgeKeytipine anahtarı ekleyin ve değeriniapp/_data/panel.tsiçindekiBADGES’te veriden türetin. - Varsayılan olarak Server Component yazın.
"use client"’ı yalnızca state, effect veya tarayıcı API’si gerçekten gerektiğinde ekleyin.
Server ve Client sınırı
Ekran bazında karar verilir, ağaç bazında değil:
- Saf sunum yapan ekranlar (
panel,apps) Server Component’tir. En ağır ekran olan dashboard bu sayede tarayıcıya bileşen JavaScript’i göndermez. - Tasarımda filtre, form veya diyalog olan ekranlar Client Component’tir.
Dashboard’daki saat bazlı ciro grafiği bir grafik kütüphanesi değil, düz flex
div’lerdir. Grafikte on üç çubuk var; eksen, tick veya tooltip yok ve taban
çizgisini tasarımın kendi 2px çizgisi çiziyor. Kütüphane eklemek hem bağımlılık
getirir hem de CSS’in zaten yaptığı iş için client sınırı açardı. Çubuk
yükseklikleri en yüksek değere göre yüzde olarak hesaplanır ve tasarımdaki sabit
yüzdeleri birebir üretir.
Kimlik doğrulama
⚠️ SCRUM-132 ile panel artık oturum ister. Yerelde çalıştırmak için
.envdosyasındaNEXT_PUBLIC_SUPABASE_URLveNEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEYtanımlı olmalı (bkz..env.example) ve Supabase projesinde bir hesabınız olmalı.
Supabase Auth (@supabase/ssr) ile, iki katmanlı bir koruma:
- Proxy (
proxy.ts→lib/supabase/proxy.ts). Her istektesupabase.auth.getClaims()çağırır; bu hem oturumu doğrular hem de süresi dolan token’ı yenileyip çerezleri günceller. Oturumsuz ziyaretçi herkese açık olmayan bir yola girerse/login?next=<yol>adresine yönlendirilir. Oturumu olan kullanıcı/login’e gelirse/panel’e gönderilir. Herkese açık yollarlib/supabase/paths.tsiçindekiisPublicPathile tek yerde tanımlıdır:/,/loginve/auth/*. - Sunucu tarafı kontrol (
lib/supabase/dal.ts→verifySession).(panel)/layout.tsxbunu çağırır; oturum yoksa/login’e yönlendirir. Next.js rehberi proxy’yi yalnızca “iyimser” bir kontrol olarak tanımladığı için veri render edilen yerde ikinci bir kontrol var. İleride gerçek veri okuyan server action ve sayfalar daverifySession’ı çağırmalı; layout kontrolü client-side navigasyonda yeniden çalışmaz.
Bunun sonucu olarak (panel) altındaki tüm rotalar artık istek başına render
edilir (build çıktısında ƒ); yalnızca / statik kalır.
Giriş yöntemleri (app/auth/actions.ts, tek login server action’ı):
- E-posta + şifre —
signInWithPassword. Başarılıysanext’e gider. - Sihirli bağlantı —
signInWithOtp,shouldCreateUser: falseile. Panel yalnızca önceden açılmış hesaplar içindir; bağlantı kendi kendine kayıt yolu olmamalı. Kayıtlı olmayan adres için de başarı mesajı gösterilir, böylece form hangi e-postaların kayıtlı olduğunu sızdırmaz. - E-postadaki bağlantı
app/auth/confirm/route.ts’ye düşer. Hem?code=(PKCE, varsayılan e-posta şablonu) hem?token_hash=&type=(özel şablon) biçimini işler. Başarısızsa/login?error=link’e döner. nextparametresi sorgu dizesinden geldiği içinsafeNextyalnızca aynı origin’deki mutlak yolları kabul eder (//evil.comgibi değerler/panel’e düşer).
Çıkış: kenar çubuğundaki “Çıkış Yap” bir <form> ile signOut server
action’ını çağırır (scope: "local", yalnızca bu tarayıcının oturumu kapanır)
ve /login’e yönlendirir.
Giriş ekranı sayfası Server Component’tir (next ve error parametrelerini
okur); form (app/login/login-form.tsx) useActionState kullanan bir client
bileşenidir.
Bilinen sınırlamalar:
- Kenar çubuğunda ad olarak
user_metadata.full_name(yoksa e-posta) gösterilir, ancak rol hâlâCURRENT_USERfixture’ından gelir. Supabase hesabı ileemployees/rol modeli arasında bir eşleme henüz yok. Kimlik paylaşımı aşağıda. - Şube (tenant) oturumdan gelmez; SCRUM-141 bekleniyor.
- Supabase Dashboard’da Redirect URLs listesine
<site>/auth/confirmeklenmedikçe sihirli bağlantı yönlendirmesi reddedilir.
Çalışan hesaplarıyla kimlik paylaşımı (değerlendirme): POS tarafındaki
personel girişi (SCRUM-183) backend’de employees tablosu üzerinden PIN ile
yapılır. Panel ise e-posta hesabıyla Supabase Auth kullanır. Bu iş ikisini
birleştirmedi: PIN, paylaşılan cihazda hızlı geçiş için tasarlandı ve internete
açık bir web paneli için tek başına yeterli değil. Olası
birleşme, employees satırına isteğe bağlı bir auth_user_id bağlayıp rolü
ve tenant’ı JWT claim’i olarak taşımaktır (SCRUM-141 ile birlikte).