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:
@@ -1,5 +1,6 @@
|
||||
import { useEffect } from '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 { ThemeToggle } from '@/components/theme-toggle'
|
||||
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
|
||||
*/
|
||||
export function AppLayout() {
|
||||
usePrefetchMainPages()
|
||||
return (
|
||||
<div className="min-h-dvh">
|
||||
<NavigationProgress />
|
||||
<Sidebar />
|
||||
|
||||
{/* 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() {
|
||||
return (
|
||||
<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
|
||||
* - `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 { MeResponse } from '@/types/MeResponse'
|
||||
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() {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.me,
|
||||
queryFn: fetchMe,
|
||||
staleTime: 5 * 60_000,
|
||||
})
|
||||
return useQuery(meQuery)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
|
||||
@@ -5,11 +5,13 @@ import { RouterProvider } from 'react-router'
|
||||
import './index.css'
|
||||
import { queryClient } from '@/lib/query'
|
||||
import { Toaster } from '@/components/toaster'
|
||||
import { prefetchMe } from '@/lib/auth'
|
||||
import { installPrintTheme } from '@/lib/print'
|
||||
import { ThemeProvider } from '@/lib/theme'
|
||||
import { router } from './router'
|
||||
|
||||
installPrintTheme()
|
||||
prefetchMe(queryClient)
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
|
||||
+89
-46
@@ -1,66 +1,106 @@
|
||||
import type { ComponentType } from 'react'
|
||||
import { createBrowserRouter, type RouteObject } from 'react-router'
|
||||
import { AppLayout } from '@/components/layout/app-layout'
|
||||
import { paths } from '@/components/layout/nav-items'
|
||||
import { RequireAuth, RequireGuest } 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'
|
||||
import { DesignPreview } from '@/features/dev/design-preview'
|
||||
import { NotFoundPage } from '@/features/errors/not-found-page'
|
||||
import { LedgerPage } from '@/features/ledger/page'
|
||||
import { MorePage } from '@/features/more/page'
|
||||
import { PeriodWizardPage } from '@/features/period-wizard/page'
|
||||
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'
|
||||
import { RequireAuth, RequireGuest, Splash } from '@/components/layout/require-auth'
|
||||
|
||||
/**
|
||||
* Sayfalar ayrı JS parçaları: yalnızca açılınca indirilir (ilk açılış küçük kalır).
|
||||
* React Router `lazy`: yeni sayfanın kodu inerken eski sayfa ekranda kalır.
|
||||
*/
|
||||
const page =
|
||||
<K extends string>(load: () => Promise<Record<K, ComponentType>>, name: K) =>
|
||||
async () => ({ Component: (await load())[name] })
|
||||
|
||||
const routes: RouteObject[] = [
|
||||
// Oturumsuz
|
||||
{
|
||||
element: <RequireGuest />,
|
||||
HydrateFallback: Splash,
|
||||
children: [
|
||||
{ path: paths.login, element: <LoginPage /> },
|
||||
{ path: paths.register, element: <RegisterPage /> },
|
||||
{ path: paths.login, lazy: page(() => import('@/features/auth/login-page'), 'LoginPage') },
|
||||
{
|
||||
path: paths.register,
|
||||
lazy: page(() => import('@/features/auth/register-page'), 'RegisterPage'),
|
||||
},
|
||||
],
|
||||
},
|
||||
// Oturumlu (iskelet: kenar çubuğu / alt menü)
|
||||
{
|
||||
element: <RequireAuth />,
|
||||
HydrateFallback: Splash,
|
||||
children: [
|
||||
{
|
||||
element: <AppLayout />,
|
||||
children: [
|
||||
{ index: true, element: <DashboardPage /> },
|
||||
{ path: paths.ledger, element: <LedgerPage /> },
|
||||
{ path: paths.planning, element: <PlanningPage /> },
|
||||
{ path: paths.periodWizard, element: <PeriodWizardPage /> },
|
||||
{ path: paths.savings, element: <SavingsPage /> },
|
||||
{ path: paths.asset, element: <AssetPage /> },
|
||||
{ path: paths.review, element: <ReviewPage /> },
|
||||
{ path: paths.closePeriod, element: <ClosePeriodPage /> },
|
||||
{ path: paths.monthlyReport, element: <MonthlyReportPage /> },
|
||||
{ path: paths.reports, element: <ReportsPage /> },
|
||||
{ path: paths.settings, element: <SettingsPage /> },
|
||||
{ path: paths.settingsCategories, element: <CategoriesPage /> },
|
||||
{ path: paths.settingsPaymentMethods, element: <PaymentMethodsPage /> },
|
||||
{ path: paths.settingsData, element: <DataPage /> },
|
||||
{ path: paths.settingsHousehold, element: <HouseholdPage /> },
|
||||
{ path: paths.settingsAccount, element: <AccountPage /> },
|
||||
{ path: paths.settingsBudget, element: <BudgetDefaultsPage /> },
|
||||
{ path: paths.statement, element: <StatementPage /> },
|
||||
{ path: paths.more, element: <MorePage /> },
|
||||
{ path: '*', element: <NotFoundPage /> },
|
||||
{ index: true, lazy: page(() => import('@/features/dashboard/page'), 'DashboardPage') },
|
||||
{ path: paths.ledger, lazy: page(() => import('@/features/ledger/page'), 'LedgerPage') },
|
||||
{
|
||||
path: paths.planning,
|
||||
lazy: page(() => import('@/features/planning/page'), 'PlanningPage'),
|
||||
},
|
||||
{
|
||||
path: paths.periodWizard,
|
||||
lazy: page(() => import('@/features/period-wizard/page'), 'PeriodWizardPage'),
|
||||
},
|
||||
{
|
||||
path: paths.savings,
|
||||
lazy: page(() => import('@/features/savings/page'), 'SavingsPage'),
|
||||
},
|
||||
{
|
||||
path: paths.asset,
|
||||
lazy: page(() => import('@/features/savings/asset-page'), 'AssetPage'),
|
||||
},
|
||||
{ path: paths.review, lazy: page(() => import('@/features/review/page'), 'ReviewPage') },
|
||||
{
|
||||
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)
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user