feat(arayüz): sayfa yönlendirme, korumalı sayfalar ve uygulama iskeleti
- react-router: Türkçe adresler (/defter, /plan, /birikim, ...), 404 sayfası - Korumalı sayfalar: oturum yoksa /giris?donus=..., girişten sonra geri dönüş - Açık yönlendirme koruması (safeReturnPath, 10 test) - İskelet: mobilde 5 öğeli alt menü, masaüstünde sol kenar çubuğu - Diğer sayfası (değerlendirme, raporlar, ayarlar, tema, çıkış) - Arayüz metinleri lib/strings.ts'de toplanmaya başlandı - Tasarım önizlemesi /dev/tasarim (yalnızca geliştirmede) - Düzeltme: çıkışta önbellek temizliği ekranı güncellemiyordu Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
import { useState, type FormEvent } from 'react'
|
||||
import { LogIn } from 'lucide-react'
|
||||
import { Link } from 'react-router'
|
||||
import { paths } from '@/components/layout/nav-items'
|
||||
import { Logo } from '@/components/logo'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { useLogin } from '@/lib/auth'
|
||||
import { useDocumentTitle } from '@/lib/use-document-title'
|
||||
|
||||
// GEÇİCİ: Faz 2 – Adım 5'te react-hook-form + zod ile gerçek giriş ekranı gelecek.
|
||||
export function LoginPage() {
|
||||
useDocumentTitle('Giriş')
|
||||
const login = useLogin()
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
|
||||
const submit = (e: FormEvent) => {
|
||||
e.preventDefault()
|
||||
// Başarılı olunca RequireGuest, ?donus= adresine yönlendirir
|
||||
login.mutate({ email, password, remember_me: true })
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="mx-auto flex min-h-dvh max-w-sm flex-col justify-center gap-6 px-4">
|
||||
<Logo size={56} withText />
|
||||
<form onSubmit={submit} className="flex flex-col gap-3">
|
||||
<input
|
||||
className="h-11 rounded-md border bg-background px-3"
|
||||
type="email"
|
||||
placeholder="E-posta"
|
||||
autoComplete="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
className="h-11 rounded-md border bg-background px-3"
|
||||
type="password"
|
||||
placeholder="Şifre"
|
||||
autoComplete="current-password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
{login.isError && <p className="text-sm text-expense">{login.error.message}</p>}
|
||||
<Button type="submit" size="lg" disabled={login.isPending}>
|
||||
<LogIn /> Giriş
|
||||
</Button>
|
||||
</form>
|
||||
<Link to={paths.register} className="text-center text-sm text-muted-foreground underline">
|
||||
Hesabınız yok mu? Kayıt olun
|
||||
</Link>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
// GEÇİCİ: Adım 5'te kayıt formu (davet kodu dahil).
|
||||
export function RegisterPage() {
|
||||
useDocumentTitle('Kayıt')
|
||||
return (
|
||||
<main className="mx-auto flex min-h-dvh max-w-sm flex-col justify-center gap-6 px-4 text-center">
|
||||
<Logo size={56} withText className="justify-center" />
|
||||
<p className="text-muted-foreground">Kayıt ekranı Faz 2 – Adım 5'te geliştirilecek.</p>
|
||||
<Link to={paths.login} className="text-sm underline">
|
||||
Girişe dön
|
||||
</Link>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { ComingSoon, PageHeader } from '@/components/page'
|
||||
import { t } from '@/lib/strings'
|
||||
|
||||
export function DashboardPage() {
|
||||
return (
|
||||
<>
|
||||
<PageHeader title={t.pages.dashboard.title} description={t.pages.dashboard.description} />
|
||||
<ComingSoon when="Faz 7'de" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import { useState } from 'react'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { Plus } from 'lucide-react'
|
||||
import { Logo } from '@/components/logo'
|
||||
import { ThemeToggle } from '@/components/theme-toggle'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { formatDate, formatMoney, formatQuantity, toIsoDate } from '@/lib/format'
|
||||
import { api } from '@/lib/api'
|
||||
import { GROUPS } from '@/lib/groups'
|
||||
import { queryKeys } from '@/lib/query'
|
||||
import type { HealthResponse } from '@/types/HealthResponse'
|
||||
|
||||
/** Tasarım sistemi önizlemesi — yalnızca geliştirmede: /dev/tasarim */
|
||||
export function DesignPreview() {
|
||||
const health = useQuery({
|
||||
queryKey: queryKeys.health,
|
||||
queryFn: () => api.get<HealthResponse>('/health'),
|
||||
})
|
||||
const [today] = useState(() => toIsoDate(new Date()))
|
||||
|
||||
return (
|
||||
<main className="mx-auto flex max-w-3xl flex-col gap-8 px-4 py-8">
|
||||
<header className="flex flex-wrap items-center justify-between gap-4">
|
||||
<div>
|
||||
<h1>
|
||||
<Logo size={44} withText />
|
||||
</h1>
|
||||
<p className="text-muted-foreground">Tasarım sistemi önizlemesi</p>
|
||||
</div>
|
||||
<ThemeToggle />
|
||||
</header>
|
||||
|
||||
<p className="text-sm">
|
||||
{health.isError && <span className="text-expense">● {health.error.message}</span>}
|
||||
{health.isPending && <span className="text-muted-foreground">● Backend kontrol ediliyor…</span>}
|
||||
{health.data && (
|
||||
<span className="text-income">
|
||||
● Backend çalışıyor (v{health.data.version}, DB: {health.data.db})
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
|
||||
|
||||
<section className="space-y-3">
|
||||
<h2 className="text-xl font-semibold">Ana gruplar</h2>
|
||||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
|
||||
{GROUPS.map((g) => (
|
||||
<div key={g.code} className={`rounded-xl p-4 ${g.className.soft}`}>
|
||||
<div className={`flex items-center gap-2 font-semibold ${g.className.text}`}>
|
||||
<span aria-hidden>{g.emoji}</span>
|
||||
{g.label}
|
||||
</div>
|
||||
<div className="mt-3 h-2 overflow-hidden rounded-full bg-background/60">
|
||||
<div className={`h-full w-2/3 rounded-full ${g.className.fill}`} />
|
||||
</div>
|
||||
<p className="mt-2 text-xs text-muted-foreground">{g.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="space-y-3">
|
||||
<h2 className="text-xl font-semibold">Tutarlar ve durum renkleri</h2>
|
||||
<div className="divide-y rounded-xl border bg-card">
|
||||
{[
|
||||
['Gelir', formatMoney(4_500_000, 'TRY', { signed: true }), 'text-income'],
|
||||
['Gider', formatMoney(-125_000), 'text-expense'],
|
||||
['Bütçenin %85\'i kullanıldı', formatMoney(85_000), 'text-warning'],
|
||||
['Euro', formatMoney(30_000, 'EUR'), ''],
|
||||
['Dolar', formatMoney(15_000, 'USD'), ''],
|
||||
['Gram altın', formatQuantity(12_500_000, 'gr'), ''],
|
||||
['Bugün', formatDate(today), ''],
|
||||
].map(([label, value, color]) => (
|
||||
<div key={label} className="flex items-center justify-between px-4 py-3">
|
||||
<span>{label}</span>
|
||||
<span className={`money font-medium ${color}`}>{value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="space-y-3">
|
||||
<h2 className="text-xl font-semibold">Butonlar</h2>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<Button size="lg">
|
||||
<Plus /> Harcama Ekle
|
||||
</Button>
|
||||
<Button size="lg" variant="secondary">
|
||||
İkincil
|
||||
</Button>
|
||||
<Button size="lg" variant="outline">
|
||||
Çerçeveli
|
||||
</Button>
|
||||
<Button size="lg" variant="destructive">
|
||||
Sil
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { House } from 'lucide-react'
|
||||
import { Link } from 'react-router'
|
||||
import { paths } from '@/components/layout/nav-items'
|
||||
import { PageHeader } from '@/components/page'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { t } from '@/lib/strings'
|
||||
|
||||
export function NotFoundPage() {
|
||||
return (
|
||||
<div className="py-10 text-center">
|
||||
<p className="mb-4 text-6xl" aria-hidden>
|
||||
🧭
|
||||
</p>
|
||||
<PageHeader title={t.pages.notFound.title} />
|
||||
<p className="-mt-4 mb-6 text-muted-foreground">{t.pages.notFound.description}</p>
|
||||
<Button asChild>
|
||||
<Link to={paths.dashboard}>
|
||||
<House /> {t.pages.notFound.action}
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { ComingSoon, PageHeader } from '@/components/page'
|
||||
import { t } from '@/lib/strings'
|
||||
|
||||
export function LedgerPage() {
|
||||
return (
|
||||
<>
|
||||
<PageHeader title={t.pages.ledger.title} description={t.pages.ledger.description} />
|
||||
<ComingSoon when="Faz 5'te" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { ChevronRight } from 'lucide-react'
|
||||
import { Link } from 'react-router'
|
||||
import { AccountSummary, LogoutButton } from '@/components/layout/app-layout'
|
||||
import { moreNav } from '@/components/layout/nav-items'
|
||||
import { PageHeader } from '@/components/page'
|
||||
import { ThemeToggle } from '@/components/theme-toggle'
|
||||
import { t } from '@/lib/strings'
|
||||
|
||||
/** Mobil "☰ Diğer": alt menüye sığmayan sayfalar, tema ve hesap. */
|
||||
export function MorePage() {
|
||||
return (
|
||||
<>
|
||||
<PageHeader title={t.pages.more.title} />
|
||||
|
||||
<nav className="divide-y overflow-hidden rounded-xl border bg-card">
|
||||
{moreNav.map(({ to, label, icon: Icon }) => (
|
||||
<Link
|
||||
key={to}
|
||||
to={to}
|
||||
className="flex min-h-14 items-center gap-3 px-4 transition-colors hover:bg-muted"
|
||||
>
|
||||
<Icon className="size-5 text-muted-foreground" aria-hidden />
|
||||
<span className="flex-1 font-medium">{label}</span>
|
||||
<ChevronRight className="size-4 text-muted-foreground" aria-hidden />
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<section className="mt-6 space-y-3">
|
||||
<h2 className="text-sm font-medium text-muted-foreground">{t.account.theme}</h2>
|
||||
<ThemeToggle className="w-full [&>button]:flex-1" />
|
||||
</section>
|
||||
|
||||
<section className="mt-6 flex items-center justify-between gap-3 rounded-xl border bg-card p-4">
|
||||
<AccountSummary />
|
||||
<LogoutButton />
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { ComingSoon, PageHeader } from '@/components/page'
|
||||
import { t } from '@/lib/strings'
|
||||
|
||||
export function PlanningPage() {
|
||||
return (
|
||||
<>
|
||||
<PageHeader title={t.pages.planning.title} description={t.pages.planning.description} />
|
||||
<ComingSoon when="Faz 4'te" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { ComingSoon, PageHeader } from '@/components/page'
|
||||
import { t } from '@/lib/strings'
|
||||
|
||||
export function ReportsPage() {
|
||||
return (
|
||||
<>
|
||||
<PageHeader title={t.pages.reports.title} description={t.pages.reports.description} />
|
||||
<ComingSoon when="Faz 9'da" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { ComingSoon, PageHeader } from '@/components/page'
|
||||
import { t } from '@/lib/strings'
|
||||
|
||||
export function ReviewPage() {
|
||||
return (
|
||||
<>
|
||||
<PageHeader title={t.pages.review.title} description={t.pages.review.description} />
|
||||
<ComingSoon when="Faz 6'da" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { ComingSoon, PageHeader } from '@/components/page'
|
||||
import { t } from '@/lib/strings'
|
||||
|
||||
export function SavingsPage() {
|
||||
return (
|
||||
<>
|
||||
<PageHeader title={t.pages.savings.title} description={t.pages.savings.description} />
|
||||
<ComingSoon when="Faz 8'de" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { ComingSoon, PageHeader } from '@/components/page'
|
||||
import { t } from '@/lib/strings'
|
||||
|
||||
export function SettingsPage() {
|
||||
return (
|
||||
<>
|
||||
<PageHeader title={t.pages.settings.title} description={t.pages.settings.description} />
|
||||
<ComingSoon when="ilerleyen fazlarda" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user