Skip to Content

Rotalar ve Kabuk

Rota tablosu

URLEkranDosyaBileşen türü
/Tanıtım (landing) sayfasıapp/(marketing)/page.tsxServer
/loginOturum açapp/login/page.tsx (+ login-form.tsx)Server (form Client)
/auth/confirmSihirli bağlantı dönüşü (route handler)app/auth/confirm/route.ts
/api/license/verifyLisans doğrulama (route handler)app/api/license/verify/route.ts
/panelPanelapp/(panel)/panel/page.tsxServer
/menuMenü Yönetimiapp/(panel)/menu/page.tsxClient
/stockStokapp/(panel)/stock/page.tsxClient
/devicesCihazlarapp/(panel)/devices/page.tsxClient
/appsUygulama Yönetimiapp/(panel)/apps/page.tsxServer
/settingsRestoran Ayarlarıapp/(panel)/settings/page.tsxClient
/staffPersonel & Rollerapp/(panel)/staff/page.tsxClient
/syncSenkronizasyonapp/(panel)/sync/page.tsxClient
/errorsHata Kayıtlarıapp/(panel)/errors/page.tsxClient
/auditDenetim Kaydıapp/(panel)/audit/page.tsxClient
/licenseLisansapp/(panel)/license/page.tsxClient
/fiscale-Belge · GİBapp/(panel)/fiscal/page.tsxClient
/billingAbonelikapp/(panel)/billing/page.tsxClient

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ından children olarak sarılır; bu ada kendi başına hiçbir şey çizmez, yalnızca işaretlemedeki data-* 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-hero alan her öğe GSAP gösterene kadar gizli başlar. Bu yüzden yeni bir data-hero değeri eklenirse landing-motion.tsx zaman ç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.ts içindeki LAUNCH sabitindedir (şu an 2027-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 saat useSyncExternalStore ile 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.ts içindedir: BRAND (“Respos”), BRAND_WORDMARK (“RESPOS”), SITE_DOMAIN (respos.com.tr), SITE_URL, CONTACT_EMAIL (iletisim@respos.com.tr) ve COMPANY (“Overfit Soft”). Kenar çubuğu, giriş ekranı, tanıtım sayfasının başlığı ve alt bilgisi ile metadata hep buradan okur; ürün adı başka hiçbir yerde sabit yazılmaz. Kök layout metadataBase’i SITE_URL yapar, tanıtım sayfası canonical ve Open Graph URL’sini / olarak verir; ikisi birlikte https://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.ts dosyasındadır. İletişim adresi site.ts’ten yeniden dışa aktarılan CONTACT_EMAIL sabitidir; “Demo talep et” ve plan düğmeleri bu adrese mailto: 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üz div’lerle çizilir ve fixture verisini “Örnek veri” etiketiyle gösterir.
  • Fiyatlar Abonelik ekranıyla aynı PLANS verisinden 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şenNeden client
_components/sidebar.tsxAktif satırı usePathname ile belirler
_components/header.tsxEkran başlığını usePathname ile belirler, şube seçicisini barındırır
_components/tenant-context.tsxSeç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: rota
  • label: kenar çubuğunda görünen ad (ör. “Menü”)
  • title ve kicker: 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 etiket
    • tone: "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

  1. app/(panel)/<segment>/page.tsx oluşturun.
  2. _screens.ts içindeki SCREENS dizisine bir kayıt ekleyin. Kenar çubuğundaki sıra dizideki sıradır.
  3. Rozet gerekiyorsa BadgeKey tipine anahtarı ekleyin ve değerini app/_data/panel.ts içindeki BADGES’te veriden türetin.
  4. 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 .env dosyasında NEXT_PUBLIC_SUPABASE_URL ve NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY tanımlı olmalı (bkz. .env.example) ve Supabase projesinde bir hesabınız olmalı.

Supabase Auth (@supabase/ssr) ile, iki katmanlı bir koruma:

  1. Proxy (proxy.tslib/supabase/proxy.ts). Her istekte supabase.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 yollar lib/supabase/paths.ts içindeki isPublicPath ile tek yerde tanımlıdır: /, /login ve /auth/*.
  2. Sunucu tarafı kontrol (lib/supabase/dal.tsverifySession). (panel)/layout.tsx bunu ç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 da verifySession’ı ç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 + şifresignInWithPassword. Başarılıysa next’e gider.
  • Sihirli bağlantısignInWithOtp, shouldCreateUser: false ile. 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.
  • next parametresi sorgu dizesinden geldiği için safeNext yalnızca aynı origin’deki mutlak yolları kabul eder (//evil.com gibi 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_USER fixture’ından gelir. Supabase hesabı ile employees/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/confirm eklenmedikç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).

Last updated on