perf(arayüz): sayfa bazlı kod bölme, oturum isteği paralel, ana sayfaları önden yükleme
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -68,7 +68,8 @@ npm run db:restore -- <dosya|latest> <hedef_db> [--force]
|
|||||||
- Frontend: Tailwind v4 (`src/index.css` token'ları), shadcn/ui (style `radix-nova`, `cn` paketi), `@/` → `src/`. Renkler: `bg-needs`/`text-needs-fg`/`bg-needs-soft` (wants, culture, unexpected aynı), `text-income`/`text-expense`/`text-warning`. SVG/JS'te (Recharts vb.) ham değişken kullan: `var(--needs)`, `var(--primary)` — `@theme inline` yüzünden `--color-*` derlenmiş CSS'te olmayabilir. Tutarlarda `money` sınıfı. Butonlar ≥44 px (shadcn boyutları büyütüldü).
|
- Frontend: Tailwind v4 (`src/index.css` token'ları), shadcn/ui (style `radix-nova`, `cn` paketi), `@/` → `src/`. Renkler: `bg-needs`/`text-needs-fg`/`bg-needs-soft` (wants, culture, unexpected aynı), `text-income`/`text-expense`/`text-warning`. SVG/JS'te (Recharts vb.) ham değişken kullan: `var(--needs)`, `var(--primary)` — `@theme inline` yüzünden `--color-*` derlenmiş CSS'te olmayabilir. Tutarlarda `money` sınıfı. Butonlar ≥44 px (shadcn boyutları büyütüldü).
|
||||||
- API: `lib/api.ts` (`api.get/post/...`, hata → `ApiError` Türkçe `message`), `lib/query.ts` (QueryClient + `queryKeys`), `lib/auth.ts` (`useMe` → undefined/null/MeResponse, `useLogin`/`useRegister`/`useLogout`).
|
- API: `lib/api.ts` (`api.get/post/...`, hata → `ApiError` Türkçe `message`), `lib/query.ts` (QueryClient + `queryKeys`), `lib/auth.ts` (`useMe` → undefined/null/MeResponse, `useLogin`/`useRegister`/`useLogout`).
|
||||||
- Tipler: `frontend/src/types/` **elle düzenlenmez**; backend'de DTO'ya `#[derive(TS)] #[ts(export)]` ekle → `npm run types`. İstek yapılarında `optional_fields`.
|
- Tipler: `frontend/src/types/` **elle düzenlenmez**; backend'de DTO'ya `#[derive(TS)] #[ts(export)]` ekle → `npm run types`. İstek yapılarında `optional_fields`.
|
||||||
- Sayfalar: `src/router.tsx`; adresler `components/layout/nav-items.ts` (`paths`); sayfa bileşenleri `features/<alan>/page.tsx`; iskelet `components/layout/app-layout.tsx`. Sayfa başlığı `PageHeader` (sekme başlığını da ayarlar). Metinler `lib/strings.ts` (`t`).
|
- Sayfalar: `src/router.tsx` — her sayfa **ayrı JS parçası** (`lazy: page(() => import(...), 'XPage')`; yeni sayfa ekleyince statik import etme). `main.tsx` `prefetchMe` (oturum isteği sayfa koduyla paralel), `AppLayout` alt menü sayfalarını boşta önden indirir + gezinmede ince ilerleme çizgisi. İlk yükleme yedeği `Splash`.
|
||||||
|
- Sayfa adresleri: adresler `components/layout/nav-items.ts` (`paths`); sayfa bileşenleri `features/<alan>/page.tsx`; iskelet `components/layout/app-layout.tsx`. Sayfa başlığı `PageHeader` (sekme başlığını da ayarlar). Metinler `lib/strings.ts` (`t`).
|
||||||
- Yönlendirme: `RequireAuth` (oturum yok → `/giris?donus=`), `RequireGuest` (oturum var → `donus` ya da panel; formlar ayrıca navigate etmez). `donus` daima `safeReturnPath` ile.
|
- Yönlendirme: `RequireAuth` (oturum yok → `/giris?donus=`), `RequireGuest` (oturum var → `donus` ya da panel; formlar ayrıca navigate etmez). `donus` daima `safeReturnPath` ile.
|
||||||
- Geliştirmede tasarım önizlemesi: `/dev/tasarim`.
|
- Geliştirmede tasarım önizlemesi: `/dev/tasarim`.
|
||||||
- Formlar: react-hook-form + zod (`mode: 'onTouched'`), alanlar `components/form-field.tsx` (`FormField`, `PasswordInput`, `FormError`); sunucu hatası `setError` ile ilgili alana. Bildirim: `toast` (sonner, `components/toaster.tsx`).
|
- Formlar: react-hook-form + zod (`mode: 'onTouched'`), alanlar `components/form-field.tsx` (`FormField`, `PasswordInput`, `FormError`); sunucu hatası `setError` ile ilgili alana. Bildirim: `toast` (sonner, `components/toaster.tsx`).
|
||||||
|
|||||||
+2
-1
@@ -1,7 +1,7 @@
|
|||||||
# 🧠 Proje Hafızası
|
# 🧠 Proje Hafızası
|
||||||
|
|
||||||
## ▶️ Sıradaki Adım
|
## ▶️ Sıradaki Adım
|
||||||
**Faz 10 – Adım 4:** Performans — `npm run build` çıktısında paket boyutlarını ölç (ana JS, Recharts/SheetJS zaten lazy), sayfa bazlı kod bölme (`React.lazy` route), Axum statik dosyalarda `/assets/*` (hash'li) için `Cache-Control: public, max-age=31536000, immutable`, `index.html` için `no-cache`; sıkıştırma zaten var. Önce/sonra ölçümü raporla.
|
**Faz 10 – Adım 5:** PWA — `vite-plugin-pwa` (gerekçe: manifest + Workbox service worker üretimi; CLAUDE.md yığınında var): manifest (ad, kısa ad, tema rengi, `display: standalone`, `start_url: /`, ikonlar `public/icon-192/512/maskable` zaten var), service worker yalnız uygulama kabuğu + hash'li `/assets/*` (precache), **`/api` asla önbelleğe alınmaz** (NetworkOnly), SPA gezinme `index.html` (navigateFallback, `/api` hariç), güncelleme bildirimi (`registerType: 'prompt'` → sonner "Yeni sürüm var — Yenile"), çevrimdışıyken anlaşılır mesaj. Axum: `sw.js` ve `manifest.webmanifest` `no-cache`. iOS: apple-touch-icon zaten var. Önce kullanıcıya kısa özet.
|
||||||
**Bekleyen (kullanıcıda):** Faz 5 – Adım 5 canlı yedek kurulumu (`docs/DEPLOY.md` §6.1–6.3) → çıktılar gelince Faz 5 ✅.
|
**Bekleyen (kullanıcıda):** Faz 5 – Adım 5 canlı yedek kurulumu (`docs/DEPLOY.md` §6.1–6.3) → çıktılar gelince Faz 5 ✅.
|
||||||
|
|
||||||
## 📌 Alınan Kararlar
|
## 📌 Alınan Kararlar
|
||||||
@@ -151,3 +151,4 @@
|
|||||||
- **Faz 10 – Adım 1 (2026-11-13):** Ayarlar menüsü (`features/settings/page.tsx`: Kategoriler, Ödeme türleri, Veri ve yedek + tema) ve alt sayfalar (`subpage.tsx`, `catalog-pages.tsx`, `data-page.tsx`); ekstre sayfası "‹ Veri ve yedek"e döner. `catalog-editor.tsx`: satırda ↑ ↓ ✎ (44 px, `aria-label` adlı), düzenleme formu ad + emoji (hızlı seçim paleti, `aria-pressed`), Pasifleştir/Yeniden etkinleştir + toast "Geri al" (sürümsüz PATCH), aynı ad hatası backend'den. `lib/catalog.ts` mutasyonları `['categories'|'payment-methods']` + rapor/dönem önbelleğini yeniler. Telefonda grup başlığı bölünüyordu → açıklama alta. Frontend 136 test; e2e 25 kontrol (DB silindi).
|
- **Faz 10 – Adım 1 (2026-11-13):** Ayarlar menüsü (`features/settings/page.tsx`: Kategoriler, Ödeme türleri, Veri ve yedek + tema) ve alt sayfalar (`subpage.tsx`, `catalog-pages.tsx`, `data-page.tsx`); ekstre sayfası "‹ Veri ve yedek"e döner. `catalog-editor.tsx`: satırda ↑ ↓ ✎ (44 px, `aria-label` adlı), düzenleme formu ad + emoji (hızlı seçim paleti, `aria-pressed`), Pasifleştir/Yeniden etkinleştir + toast "Geri al" (sürümsüz PATCH), aynı ad hatası backend'den. `lib/catalog.ts` mutasyonları `['categories'|'payment-methods']` + rapor/dönem önbelleğini yeniler. Telefonda grup başlığı bölünüyordu → açıklama alta. Frontend 136 test; e2e 25 kontrol (DB silindi).
|
||||||
- **Faz 10 – Adım 2 (2026-11-13):** Backend: `PATCH /auth/me` (ad), `POST /auth/me/password` (mevcut şifre + limiter `password:<uid>`, `sessions_valid_from` → diğer oturumlar 401, bu cihaza yeni çerez, "beni hatırla" eski çerezin `exp-iat`'ından), `PATCH /household` (sahip: ad, `budget_split_pct` toplam 100, `budget_suggestion`), `DELETE /household/members/{id}` (sahip; kendini/sahibi çıkaramaz; e-posta `#removed-<id>` → yeniden davet edilebilir). `GET /household` artık `settings` döner. `services/account.rs`. Testler `tests/account.rs` (4) + 2 bilinçli bozma yakalandı. Frontend: `/ayarlar/hane` (ad, üyeler + çıkar onaylı, davet oluştur → panoya kopyala / paylaş / iptal), `/ayarlar/hesap` (ad, e-posta salt okunur, şifre formu: zod tekrar + farklı, sunucu hatası alana), `/ayarlar/butce` (4 yüzde canlı toplam, 50/30/10/10 düğmesi, yöntem radyo; üye salt okunur). Öneri seçici ve sihirbaz hanenin yöntemiyle açılır (veri yoksa yüzdelere düşer). E2E 28 kontrol: **davet bağlantısıyla gerçek kayıt** dahil (`REGISTRATION=invite_only`). Not: başsız Chrome'da pano için `Emulation.setFocusEmulationEnabled`. Backend 205, frontend 140 test.
|
- **Faz 10 – Adım 2 (2026-11-13):** Backend: `PATCH /auth/me` (ad), `POST /auth/me/password` (mevcut şifre + limiter `password:<uid>`, `sessions_valid_from` → diğer oturumlar 401, bu cihaza yeni çerez, "beni hatırla" eski çerezin `exp-iat`'ından), `PATCH /household` (sahip: ad, `budget_split_pct` toplam 100, `budget_suggestion`), `DELETE /household/members/{id}` (sahip; kendini/sahibi çıkaramaz; e-posta `#removed-<id>` → yeniden davet edilebilir). `GET /household` artık `settings` döner. `services/account.rs`. Testler `tests/account.rs` (4) + 2 bilinçli bozma yakalandı. Frontend: `/ayarlar/hane` (ad, üyeler + çıkar onaylı, davet oluştur → panoya kopyala / paylaş / iptal), `/ayarlar/hesap` (ad, e-posta salt okunur, şifre formu: zod tekrar + farklı, sunucu hatası alana), `/ayarlar/butce` (4 yüzde canlı toplam, 50/30/10/10 düğmesi, yöntem radyo; üye salt okunur). Öneri seçici ve sihirbaz hanenin yöntemiyle açılır (veri yoksa yüzdelere düşer). E2E 28 kontrol: **davet bağlantısıyla gerçek kayıt** dahil (`REGISTRATION=invite_only`). Not: başsız Chrome'da pano için `Emulation.setFocusEmulationEnabled`. Backend 205, frontend 140 test.
|
||||||
- **Faz 10 – Adım 3 (2026-11-13):** Tarama betiği (scratchpad `e2e/scan.mjs`): 21 sayfa × telefon açık/koyu + masaüstü (+ boş hane, giriş/kayıt); sayfa içi denetim (adsız denetim, <44 px hedef, canvas ile oklch→sRGB kontrast, tek h1, alt, lang, taşma) + Tab ile odak halkası. İlk sonuç 89 kontrast / 75 küçük hedef / 27 adsız / 3 h1 → benzersiz: muted yazı 4,35 (→ token 0.52), yeşil 4,37 (→ #166534), `sm` düğmeler 36 px (→ pointer-coarse 44), GFM görev kutusu adsız (→ "Tamamlandı/Yapılacak"), rapor 2 h1 (→ `shiftHeadings`). **Ekran görüntüsünden:** telefonda Defter formu taşıyordu (ızgara öğesi `min-width:auto` 425 px) — otomatik kontrol kaçırdı çünkü mobil öykünmede `innerWidth` da büyür → kontrol `screen.width`/390 ile; Raporlar grafik kartları da taşıyordu → `Section` + rapor bölümüne `min-w-0`. Eklenen: %80 bütçe uyarısı (`lib/budget-level.ts`), Raporlar boş durumu (dönem yok → Dönemi başlat / Ekstreden aktar; filtre boş → Filtreleri temizle), tutarlarda bölünmez boşluk. Sonra: kontrast/adsız/h1/telefon hedef 0. Frontend 142 test.
|
- **Faz 10 – Adım 3 (2026-11-13):** Tarama betiği (scratchpad `e2e/scan.mjs`): 21 sayfa × telefon açık/koyu + masaüstü (+ boş hane, giriş/kayıt); sayfa içi denetim (adsız denetim, <44 px hedef, canvas ile oklch→sRGB kontrast, tek h1, alt, lang, taşma) + Tab ile odak halkası. İlk sonuç 89 kontrast / 75 küçük hedef / 27 adsız / 3 h1 → benzersiz: muted yazı 4,35 (→ token 0.52), yeşil 4,37 (→ #166534), `sm` düğmeler 36 px (→ pointer-coarse 44), GFM görev kutusu adsız (→ "Tamamlandı/Yapılacak"), rapor 2 h1 (→ `shiftHeadings`). **Ekran görüntüsünden:** telefonda Defter formu taşıyordu (ızgara öğesi `min-width:auto` 425 px) — otomatik kontrol kaçırdı çünkü mobil öykünmede `innerWidth` da büyür → kontrol `screen.width`/390 ile; Raporlar grafik kartları da taşıyordu → `Section` + rapor bölümüne `min-w-0`. Eklenen: %80 bütçe uyarısı (`lib/budget-level.ts`), Raporlar boş durumu (dönem yok → Dönemi başlat / Ekstreden aktar; filtre boş → Filtreleri temizle), tutarlarda bölünmez boşluk. Sonra: kontrast/adsız/h1/telefon hedef 0. Frontend 142 test.
|
||||||
|
- **Faz 10 – Adım 4 (2026-11-13):** Önbellek başlıkları zaten doğruydu (Faz 5: `/assets/*` 1 yıl immutable, HTML no-cache, sıkıştırma). Ana JS 950 KB (gz 284) tek paket → sayfa bazlı `lazy` route: ana 402 KB (gz 128), sayfalar 2–23 KB; Markdown (49 KB gz) ve zod/form (40 KB gz) yalnız kullanan sayfada. İlk sürümde Defter yavaşlamıştı (lazy route → `/auth/me` sıralı) → `prefetchMe` ile paralel; `HydrateFallback: Splash`. Boşta alt menü sayfalarını önden indirme + `useNavigation` ilerleme çizgisi. Ölçüm (scratchpad `e2e/perf.mjs`: yavaş 4G 1,6 Mbps/150 ms + CPU 4×, soğuk; eski sürüm `git worktree` ile ayrı derlenip aynı koşulda, 2 tekrar ort.): Giriş 2,97→2,23 sn, Panel 2,42→2,03, Raporlar 2,36→2,11, Defter 2,33→2,37 (aynı); hazırda JS 295→171–221 KB. Regresyon: katalog (25) + rapor (21) e2e geçti. Not: e2e rapor başlığı artık `.md-h1`.
|
||||||
|
|||||||
+2
-2
@@ -90,5 +90,5 @@
|
|||||||
- ✅ 1. Ayarlar yeniden düzeni (`/ayarlar` menü + tema) · **Kategori yönetimi** (grup grup ekle, ad/emoji düzenle, sırala, pasifleştir) · **Ödeme türü yönetimi** · Veri sayfası `/ayarlar/veri`
|
- ✅ 1. Ayarlar yeniden düzeni (`/ayarlar` menü + tema) · **Kategori yönetimi** (grup grup ekle, ad/emoji düzenle, sırala, pasifleştir) · **Ödeme türü yönetimi** · Veri sayfası `/ayarlar/veri`
|
||||||
- ✅ 2. Hane ve hesap: hane adı, üyeler, **davet bağlantısı** (oluştur/kopyala/iptal), üye çıkarma; profil adı, **şifre değiştirme**; varsayılan bütçe yüzdeleri + öneri yöntemi
|
- ✅ 2. Hane ve hesap: hane adı, üyeler, **davet bağlantısı** (oluştur/kopyala/iptal), üye çıkarma; profil adı, **şifre değiştirme**; varsayılan bütçe yüzdeleri + öneri yöntemi
|
||||||
- ✅ 3. Arayüz cilası ve erişilebilirlik: tüm sayfaların telefon/masaüstü/koyu tema taraması, boş durumlar, skeleton, %80 bütçe uyarısı, odak/klavye, kontrast, onay diyalogları
|
- ✅ 3. Arayüz cilası ve erişilebilirlik: tüm sayfaların telefon/masaüstü/koyu tema taraması, boş durumlar, skeleton, %80 bütçe uyarısı, odak/klavye, kontrast, onay diyalogları
|
||||||
- 🔄 4. Performans: sayfa bazlı kod bölme (lazy route), statik dosyalarda uzun önbellek, paket boyutu
|
- ✅ 4. Performans: sayfa bazlı kod bölme (lazy route), statik dosyalarda uzun önbellek, paket boyutu
|
||||||
- ⏳ 5. PWA: manifest + service worker (uygulama kabuğu; `/api` asla önbelleğe alınmaz), kurulum, güncelleme bildirimi
|
- 🔄 5. PWA: manifest + service worker (uygulama kabuğu; `/api` asla önbelleğe alınmaz), kurulum, güncelleme bildirimi
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
|
import { useEffect } from 'react'
|
||||||
import { LogOut } from 'lucide-react'
|
import { LogOut } from 'lucide-react'
|
||||||
import { NavLink, Outlet, ScrollRestoration, useLocation } from 'react-router'
|
import { NavLink, Outlet, ScrollRestoration, useLocation, useNavigation } from 'react-router'
|
||||||
import { Logo } from '@/components/logo'
|
import { Logo } from '@/components/logo'
|
||||||
import { ThemeToggle } from '@/components/theme-toggle'
|
import { ThemeToggle } from '@/components/theme-toggle'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
@@ -14,8 +15,10 @@ import { bottomNav, moreNav, paths, sideNav } from './nav-items'
|
|||||||
* - Masaüstü (≥ md): solda kenar çubuğu
|
* - Masaüstü (≥ md): solda kenar çubuğu
|
||||||
*/
|
*/
|
||||||
export function AppLayout() {
|
export function AppLayout() {
|
||||||
|
usePrefetchMainPages()
|
||||||
return (
|
return (
|
||||||
<div className="min-h-dvh">
|
<div className="min-h-dvh">
|
||||||
|
<NavigationProgress />
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
|
|
||||||
{/* Mobil üst çubuk */}
|
{/* Mobil üst çubuk */}
|
||||||
@@ -37,6 +40,45 @@ export function AppLayout() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alt menüdeki ana sayfaların kodu, tarayıcı boşta kalınca önceden indirilir
|
||||||
|
* (ilk açılışı yavaşlatmaz; menüde gezinirken bekleme olmaz).
|
||||||
|
*/
|
||||||
|
function usePrefetchMainPages() {
|
||||||
|
useEffect(() => {
|
||||||
|
const load = () => {
|
||||||
|
void import('@/features/dashboard/page')
|
||||||
|
void import('@/features/ledger/page')
|
||||||
|
void import('@/features/planning/page')
|
||||||
|
void import('@/features/savings/page')
|
||||||
|
}
|
||||||
|
if ('requestIdleCallback' in window) {
|
||||||
|
const id = window.requestIdleCallback(load, { timeout: 5000 })
|
||||||
|
return () => window.cancelIdleCallback(id)
|
||||||
|
}
|
||||||
|
const id = setTimeout(load, 2000)
|
||||||
|
return () => clearTimeout(id)
|
||||||
|
}, [])
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sayfa kodu inerken (yavaş bağlantı) üstte ince ilerleme çizgisi; anlık geçişte görünmez */
|
||||||
|
function NavigationProgress() {
|
||||||
|
const loading = useNavigation().state === 'loading'
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="progressbar"
|
||||||
|
aria-label={t.common.loading}
|
||||||
|
aria-hidden={!loading}
|
||||||
|
className={cn(
|
||||||
|
'pointer-events-none fixed inset-x-0 top-0 z-50 h-0.5 origin-left bg-primary transition-[transform,opacity] print:hidden',
|
||||||
|
loading
|
||||||
|
? 'scale-x-75 opacity-100 delay-150 duration-[2s] ease-out'
|
||||||
|
: 'scale-x-100 opacity-0 duration-300',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function Sidebar() {
|
function Sidebar() {
|
||||||
return (
|
return (
|
||||||
<aside className="fixed inset-y-0 left-0 z-30 hidden w-64 flex-col border-r bg-sidebar md:flex print:hidden">
|
<aside className="fixed inset-y-0 left-0 z-30 hidden w-64 flex-col border-r bg-sidebar md:flex print:hidden">
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
* - `null` → oturum yok
|
* - `null` → oturum yok
|
||||||
* - `MeResponse`→ giriş yapılmış
|
* - `MeResponse`→ giriş yapılmış
|
||||||
*/
|
*/
|
||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
import { useMutation, useQuery, useQueryClient, type QueryClient } from '@tanstack/react-query'
|
||||||
import type { LoginRequest } from '@/types/LoginRequest'
|
import type { LoginRequest } from '@/types/LoginRequest'
|
||||||
import type { MeResponse } from '@/types/MeResponse'
|
import type { MeResponse } from '@/types/MeResponse'
|
||||||
import type { RegisterRequest } from '@/types/RegisterRequest'
|
import type { RegisterRequest } from '@/types/RegisterRequest'
|
||||||
@@ -23,12 +23,18 @@ async function fetchMe(): Promise<MeResponse | null> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const meQuery = { queryKey: queryKeys.me, queryFn: fetchMe, staleTime: 5 * 60_000 }
|
||||||
|
|
||||||
export function useMe() {
|
export function useMe() {
|
||||||
return useQuery({
|
return useQuery(meQuery)
|
||||||
queryKey: queryKeys.me,
|
}
|
||||||
queryFn: fetchMe,
|
|
||||||
staleTime: 5 * 60_000,
|
/**
|
||||||
})
|
* Açılışta oturumu hemen sor: sayfa kodu (ayrı JS parçası) inerken istek paralel yürür,
|
||||||
|
* sıralı bekleme olmaz. `useMe` aynı sorguyu önbellekten alır.
|
||||||
|
*/
|
||||||
|
export function prefetchMe(client: QueryClient) {
|
||||||
|
void client.prefetchQuery(meQuery)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useLogin() {
|
export function useLogin() {
|
||||||
|
|||||||
@@ -5,11 +5,13 @@ import { RouterProvider } from 'react-router'
|
|||||||
import './index.css'
|
import './index.css'
|
||||||
import { queryClient } from '@/lib/query'
|
import { queryClient } from '@/lib/query'
|
||||||
import { Toaster } from '@/components/toaster'
|
import { Toaster } from '@/components/toaster'
|
||||||
|
import { prefetchMe } from '@/lib/auth'
|
||||||
import { installPrintTheme } from '@/lib/print'
|
import { installPrintTheme } from '@/lib/print'
|
||||||
import { ThemeProvider } from '@/lib/theme'
|
import { ThemeProvider } from '@/lib/theme'
|
||||||
import { router } from './router'
|
import { router } from './router'
|
||||||
|
|
||||||
installPrintTheme()
|
installPrintTheme()
|
||||||
|
prefetchMe(queryClient)
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById('root')!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
|
|||||||
+89
-46
@@ -1,66 +1,106 @@
|
|||||||
|
import type { ComponentType } from 'react'
|
||||||
import { createBrowserRouter, type RouteObject } from 'react-router'
|
import { createBrowserRouter, type RouteObject } from 'react-router'
|
||||||
import { AppLayout } from '@/components/layout/app-layout'
|
import { AppLayout } from '@/components/layout/app-layout'
|
||||||
import { paths } from '@/components/layout/nav-items'
|
import { paths } from '@/components/layout/nav-items'
|
||||||
import { RequireAuth, RequireGuest } from '@/components/layout/require-auth'
|
import { RequireAuth, RequireGuest, Splash } from '@/components/layout/require-auth'
|
||||||
import { LoginPage } from '@/features/auth/login-page'
|
|
||||||
import { RegisterPage } from '@/features/auth/register-page'
|
/**
|
||||||
import { DashboardPage } from '@/features/dashboard/page'
|
* Sayfalar ayrı JS parçaları: yalnızca açılınca indirilir (ilk açılış küçük kalır).
|
||||||
import { DesignPreview } from '@/features/dev/design-preview'
|
* React Router `lazy`: yeni sayfanın kodu inerken eski sayfa ekranda kalır.
|
||||||
import { NotFoundPage } from '@/features/errors/not-found-page'
|
*/
|
||||||
import { LedgerPage } from '@/features/ledger/page'
|
const page =
|
||||||
import { MorePage } from '@/features/more/page'
|
<K extends string>(load: () => Promise<Record<K, ComponentType>>, name: K) =>
|
||||||
import { PeriodWizardPage } from '@/features/period-wizard/page'
|
async () => ({ Component: (await load())[name] })
|
||||||
import { PlanningPage } from '@/features/planning/page'
|
|
||||||
import { ReportsPage } from '@/features/reports/page'
|
|
||||||
import { ClosePeriodPage } from '@/features/review/close-page'
|
|
||||||
import { MonthlyReportPage } from '@/features/review/report-page'
|
|
||||||
import { ReviewPage } from '@/features/review/page'
|
|
||||||
import { AssetPage } from '@/features/savings/asset-page'
|
|
||||||
import { SavingsPage } from '@/features/savings/page'
|
|
||||||
import { CategoriesPage, PaymentMethodsPage } from '@/features/settings/catalog-pages'
|
|
||||||
import { AccountPage } from '@/features/settings/account-page'
|
|
||||||
import { BudgetDefaultsPage } from '@/features/settings/budget-page'
|
|
||||||
import { DataPage } from '@/features/settings/data-page'
|
|
||||||
import { HouseholdPage } from '@/features/settings/household-page'
|
|
||||||
import { SettingsPage } from '@/features/settings/page'
|
|
||||||
import { StatementPage } from '@/features/statement/page'
|
|
||||||
|
|
||||||
const routes: RouteObject[] = [
|
const routes: RouteObject[] = [
|
||||||
// Oturumsuz
|
// Oturumsuz
|
||||||
{
|
{
|
||||||
element: <RequireGuest />,
|
element: <RequireGuest />,
|
||||||
|
HydrateFallback: Splash,
|
||||||
children: [
|
children: [
|
||||||
{ path: paths.login, element: <LoginPage /> },
|
{ path: paths.login, lazy: page(() => import('@/features/auth/login-page'), 'LoginPage') },
|
||||||
{ path: paths.register, element: <RegisterPage /> },
|
{
|
||||||
|
path: paths.register,
|
||||||
|
lazy: page(() => import('@/features/auth/register-page'), 'RegisterPage'),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
// Oturumlu (iskelet: kenar çubuğu / alt menü)
|
// Oturumlu (iskelet: kenar çubuğu / alt menü)
|
||||||
{
|
{
|
||||||
element: <RequireAuth />,
|
element: <RequireAuth />,
|
||||||
|
HydrateFallback: Splash,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
element: <AppLayout />,
|
element: <AppLayout />,
|
||||||
children: [
|
children: [
|
||||||
{ index: true, element: <DashboardPage /> },
|
{ index: true, lazy: page(() => import('@/features/dashboard/page'), 'DashboardPage') },
|
||||||
{ path: paths.ledger, element: <LedgerPage /> },
|
{ path: paths.ledger, lazy: page(() => import('@/features/ledger/page'), 'LedgerPage') },
|
||||||
{ path: paths.planning, element: <PlanningPage /> },
|
{
|
||||||
{ path: paths.periodWizard, element: <PeriodWizardPage /> },
|
path: paths.planning,
|
||||||
{ path: paths.savings, element: <SavingsPage /> },
|
lazy: page(() => import('@/features/planning/page'), 'PlanningPage'),
|
||||||
{ path: paths.asset, element: <AssetPage /> },
|
},
|
||||||
{ path: paths.review, element: <ReviewPage /> },
|
{
|
||||||
{ path: paths.closePeriod, element: <ClosePeriodPage /> },
|
path: paths.periodWizard,
|
||||||
{ path: paths.monthlyReport, element: <MonthlyReportPage /> },
|
lazy: page(() => import('@/features/period-wizard/page'), 'PeriodWizardPage'),
|
||||||
{ path: paths.reports, element: <ReportsPage /> },
|
},
|
||||||
{ path: paths.settings, element: <SettingsPage /> },
|
{
|
||||||
{ path: paths.settingsCategories, element: <CategoriesPage /> },
|
path: paths.savings,
|
||||||
{ path: paths.settingsPaymentMethods, element: <PaymentMethodsPage /> },
|
lazy: page(() => import('@/features/savings/page'), 'SavingsPage'),
|
||||||
{ path: paths.settingsData, element: <DataPage /> },
|
},
|
||||||
{ path: paths.settingsHousehold, element: <HouseholdPage /> },
|
{
|
||||||
{ path: paths.settingsAccount, element: <AccountPage /> },
|
path: paths.asset,
|
||||||
{ path: paths.settingsBudget, element: <BudgetDefaultsPage /> },
|
lazy: page(() => import('@/features/savings/asset-page'), 'AssetPage'),
|
||||||
{ path: paths.statement, element: <StatementPage /> },
|
},
|
||||||
{ path: paths.more, element: <MorePage /> },
|
{ path: paths.review, lazy: page(() => import('@/features/review/page'), 'ReviewPage') },
|
||||||
{ path: '*', element: <NotFoundPage /> },
|
{
|
||||||
|
path: paths.closePeriod,
|
||||||
|
lazy: page(() => import('@/features/review/close-page'), 'ClosePeriodPage'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: paths.monthlyReport,
|
||||||
|
lazy: page(() => import('@/features/review/report-page'), 'MonthlyReportPage'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: paths.reports,
|
||||||
|
lazy: page(() => import('@/features/reports/page'), 'ReportsPage'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: paths.settings,
|
||||||
|
lazy: page(() => import('@/features/settings/page'), 'SettingsPage'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: paths.settingsCategories,
|
||||||
|
lazy: page(() => import('@/features/settings/catalog-pages'), 'CategoriesPage'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: paths.settingsPaymentMethods,
|
||||||
|
lazy: page(() => import('@/features/settings/catalog-pages'), 'PaymentMethodsPage'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: paths.settingsData,
|
||||||
|
lazy: page(() => import('@/features/settings/data-page'), 'DataPage'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: paths.settingsHousehold,
|
||||||
|
lazy: page(() => import('@/features/settings/household-page'), 'HouseholdPage'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: paths.settingsAccount,
|
||||||
|
lazy: page(() => import('@/features/settings/account-page'), 'AccountPage'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: paths.settingsBudget,
|
||||||
|
lazy: page(() => import('@/features/settings/budget-page'), 'BudgetDefaultsPage'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: paths.statement,
|
||||||
|
lazy: page(() => import('@/features/statement/page'), 'StatementPage'),
|
||||||
|
},
|
||||||
|
{ path: paths.more, lazy: page(() => import('@/features/more/page'), 'MorePage') },
|
||||||
|
{
|
||||||
|
path: '*',
|
||||||
|
lazy: page(() => import('@/features/errors/not-found-page'), 'NotFoundPage'),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -69,7 +109,10 @@ const routes: RouteObject[] = [
|
|||||||
|
|
||||||
// Tasarım önizlemesi yalnızca geliştirmede (canlı derlemeye girmez)
|
// Tasarım önizlemesi yalnızca geliştirmede (canlı derlemeye girmez)
|
||||||
if (import.meta.env.DEV) {
|
if (import.meta.env.DEV) {
|
||||||
routes.unshift({ path: '/dev/tasarim', element: <DesignPreview /> })
|
routes.unshift({
|
||||||
|
path: '/dev/tasarim',
|
||||||
|
lazy: page(() => import('@/features/dev/design-preview'), 'DesignPreview'),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const router = createBrowserRouter(routes)
|
export const router = createBrowserRouter(routes)
|
||||||
|
|||||||
Reference in New Issue
Block a user