diff --git a/src/app/(app)/dashboard/loading.tsx b/src/app/(app)/dashboard/loading.tsx
new file mode 100644
index 0000000..d1e7bb2
--- /dev/null
+++ b/src/app/(app)/dashboard/loading.tsx
@@ -0,0 +1,14 @@
+import { Skeleton } from "@/components/ui/skeleton";
+import { SummaryCardsSkeleton } from "@/features/dashboard/components/summary-cards-skeleton";
+
+export default function DashboardLoading() {
+ return (
+ <>
+
+
+
+
+
+ >
+ );
+}
diff --git a/src/app/(app)/dashboard/page.tsx b/src/app/(app)/dashboard/page.tsx
new file mode 100644
index 0000000..7d40c44
--- /dev/null
+++ b/src/app/(app)/dashboard/page.tsx
@@ -0,0 +1,67 @@
+import type { Metadata } from "next";
+import { Suspense } from "react";
+import { PageHeader } from "@/components/page-header";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import { MOCK_SESSION_USER } from "@/features/auth/mock";
+import { SummaryCardsSkeleton } from "@/features/dashboard/components/summary-cards-skeleton";
+import { tr } from "@/i18n/tr";
+import { cn } from "@/lib/utils";
+import { formatMoney } from "@/shared/format";
+
+export const metadata: Metadata = { title: tr.dashboard.title };
+
+type SummaryCard = {
+ key: keyof typeof tr.dashboard.cards;
+ tone?: string;
+ amountMinor: number | null;
+};
+
+/** FAZ 14: gerçek özet verisi (gelir, gider, bakiye, kart borcu, net varlık). */
+async function getSummary(): Promise {
+ return [
+ { key: "monthIncome", tone: "text-income", amountMinor: null },
+ { key: "monthExpense", tone: "text-expense", amountMinor: null },
+ { key: "monthRemaining", tone: "text-budget", amountMinor: null },
+ { key: "totalBalance", amountMinor: null },
+ { key: "cardDebt", tone: "text-expense", amountMinor: null },
+ { key: "netWorth", amountMinor: null },
+ ];
+}
+
+async function SummaryCards() {
+ const cards = await getSummary();
+ return (
+
+ {cards.map((c) => (
+
+
+ {tr.dashboard.cards[c.key]}
+
+ {c.amountMinor === null ? "—" : formatMoney(c.amountMinor)}
+
+
+ {c.amountMinor === null ? (
+
+ {tr.dashboard.noData}
+
+ ) : null}
+
+ ))}
+
+ );
+}
+
+export default function DashboardPage() {
+ const user = MOCK_SESSION_USER;
+ return (
+ <>
+
+ }>
+
+
+ >
+ );
+}
diff --git a/src/app/(app)/layout.tsx b/src/app/(app)/layout.tsx
new file mode 100644
index 0000000..0aa1c1b
--- /dev/null
+++ b/src/app/(app)/layout.tsx
@@ -0,0 +1,29 @@
+import { AppHeader } from "@/components/layout/app-header";
+import { AppSidebar } from "@/components/layout/app-sidebar";
+import { BottomNav } from "@/components/layout/bottom-nav";
+import { QuickAddProvider } from "@/components/layout/quick-add";
+import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
+import { MOCK_SESSION_USER } from "@/features/auth/mock";
+import { MOCK_HOUSEHOLDS } from "@/features/households/mock";
+import { APP_VERSION } from "@/lib/version";
+
+/**
+ * Oturum gerektiren alan: yan menü (masaüstü), üst bar, alt menü (mobil), hızlı ekle.
+ * FAZ 3: `const session = await requireSession()` — oturum yoksa /login.
+ */
+export default function AppLayout({ children }: { children: React.ReactNode }) {
+ const user = MOCK_SESSION_USER;
+ return (
+
+
+
+
+
+ {/* Mobilde alt menü yüksekliği kadar boşluk */}
+ {children}
+
+
+
+
+ );
+}
diff --git a/src/app/(auth)/layout.tsx b/src/app/(auth)/layout.tsx
new file mode 100644
index 0000000..a1c295c
--- /dev/null
+++ b/src/app/(auth)/layout.tsx
@@ -0,0 +1,13 @@
+import { ThemeToggle } from "@/components/layout/theme-toggle";
+
+/** Oturum gerektirmeyen ekranlar (yalnızca giriş). */
+export default function AuthLayout({ children }: { children: React.ReactNode }) {
+ return (
+
+ );
+}
diff --git a/src/app/(auth)/login/login-form.tsx b/src/app/(auth)/login/login-form.tsx
new file mode 100644
index 0000000..3d0e25a
--- /dev/null
+++ b/src/app/(auth)/login/login-form.tsx
@@ -0,0 +1,81 @@
+"use client";
+
+import { zodResolver } from "@hookform/resolvers/zod";
+import { Loader2 } from "lucide-react";
+import { useRouter } from "next/navigation";
+import { useState } from "react";
+import { Controller, useForm } from "react-hook-form";
+import { PasswordInput } from "@/components/password-input";
+import { Button } from "@/components/ui/button";
+import { Field, FieldError, FieldGroup, FieldLabel } from "@/components/ui/field";
+import { Input } from "@/components/ui/input";
+import { tr } from "@/i18n/tr";
+import { loginSchema, type LoginInput } from "@/shared/schemas/auth";
+
+export function LoginForm() {
+ const router = useRouter();
+ const [formError, setFormError] = useState(null);
+ const form = useForm({
+ resolver: zodResolver(loginSchema),
+ defaultValues: { identifier: "", password: "" },
+ });
+
+ async function onSubmit(_values: LoginInput) {
+ setFormError(null);
+ // FAZ 3: looksLikeEmail(identifier) ? authClient.signIn.email : authClient.signIn.username
+ await new Promise((r) => setTimeout(r, 400));
+ router.replace("/dashboard");
+ }
+
+ const submitting = form.formState.isSubmitting;
+
+ return (
+
+ );
+}
diff --git a/src/app/(auth)/login/page.tsx b/src/app/(auth)/login/page.tsx
new file mode 100644
index 0000000..1c00480
--- /dev/null
+++ b/src/app/(auth)/login/page.tsx
@@ -0,0 +1,25 @@
+import type { Metadata } from "next";
+import { Logo } from "@/components/brand/logo";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import { tr } from "@/i18n/tr";
+import { LoginForm } from "./login-form";
+
+export const metadata: Metadata = { title: tr.auth.loginTitle };
+
+export default function LoginPage() {
+ return (
+
+
+
+
+ {tr.auth.loginTitle}
+ {tr.auth.loginDescription}
+
+
+
+
+
+ {tr.auth.noSignUp}
+
+ );
+}
diff --git a/src/app/error.tsx b/src/app/error.tsx
new file mode 100644
index 0000000..084a220
--- /dev/null
+++ b/src/app/error.tsx
@@ -0,0 +1,27 @@
+"use client";
+
+import { useEffect } from "react";
+import { Button } from "@/components/ui/button";
+import { tr } from "@/i18n/tr";
+
+/** Rota hata sınırı. İç detay gösterilmez; yalnızca sunucu loglarıyla eşleşen hata kodu (digest). */
+export default function ErrorPage({ error, retry }: { error: unknown; retry: () => void }) {
+ useEffect(() => {
+ console.error(error);
+ }, [error]);
+
+ const digest = error instanceof Error && "digest" in error ? String(error.digest) : undefined;
+
+ return (
+
+ {tr.errors.errorTitle}
+ {tr.errors.errorDescription}
+ {digest ? (
+
+ {tr.errors.reference}: {digest}
+
+ ) : null}
+
+
+ );
+}
diff --git a/src/app/global-error.tsx b/src/app/global-error.tsx
new file mode 100644
index 0000000..2f84f61
--- /dev/null
+++ b/src/app/global-error.tsx
@@ -0,0 +1,23 @@
+"use client";
+
+import { tr } from "@/i18n/tr";
+import "./globals.css";
+
+/** Kök layout hataları için son çare sınırı (kendi / etiketini render eder). */
+export default function GlobalError({ retry }: { error: unknown; retry: () => void }) {
+ return (
+
+
+ {tr.errors.errorTitle}
+ {tr.errors.errorDescription}
+
+
+
+ );
+}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index b6038a9..6d12f99 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -1,5 +1,6 @@
import type { Metadata, Viewport } from "next";
import { Geist, Geist_Mono } from "next/font/google";
+import { Providers } from "@/components/providers";
import { tr } from "@/i18n/tr";
import "./globals.css";
@@ -18,17 +19,31 @@ export const metadata: Metadata = {
title: { default: tr.app.name, template: `%s · ${tr.app.name}` },
description: tr.app.tagline,
applicationName: tr.app.name,
+ appleWebApp: { capable: true, title: tr.app.name, statusBarStyle: "default" },
+ formatDetection: { telephone: false },
};
export const viewport: Viewport = {
width: "device-width",
initialScale: 1,
+ viewportFit: "cover",
+ // Manifest/tarayıcı çubuğu rengi CSS değişkeni okuyamaz; --background değerlerinin hex karşılığı.
+ themeColor: [
+ { media: "(prefers-color-scheme: light)", color: "#fafcfe" },
+ { media: "(prefers-color-scheme: dark)", color: "#081123" },
+ ],
};
export default function RootLayout({ children }: LayoutProps<"/">) {
return (
-
- {children}
+
+
+ {children}
+
);
}
diff --git a/src/app/loading.tsx b/src/app/loading.tsx
new file mode 100644
index 0000000..b48912b
--- /dev/null
+++ b/src/app/loading.tsx
@@ -0,0 +1,14 @@
+import { Loader2 } from "lucide-react";
+import { tr } from "@/i18n/tr";
+
+export default function Loading() {
+ return (
+
+
+ {tr.common.loading}
+
+ );
+}
diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx
new file mode 100644
index 0000000..140a90c
--- /dev/null
+++ b/src/app/not-found.tsx
@@ -0,0 +1,16 @@
+import Link from "next/link";
+import { Button } from "@/components/ui/button";
+import { tr } from "@/i18n/tr";
+
+export default function NotFound() {
+ return (
+
+ 404
+ {tr.errors.notFoundTitle}
+ {tr.errors.notFoundDescription}
+
+
+ );
+}
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 2b1508c..0abc6c8 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -1,15 +1,6 @@
-import { tr } from "@/i18n/tr";
-import { APP_VERSION } from "@/lib/version";
+import { redirect } from "next/navigation";
-/** Geçici açılış sayfası — Faz 2'de /login'e yönlendirme ile değişecek. */
+/** Uygulama her zaman login ile açılır. FAZ 3: oturum varsa /dashboard'a yönlenecek. */
export default function Home() {
- return (
-
- {tr.app.name}
- {tr.app.setupDescription}
-
- {tr.app.version} {APP_VERSION}
-
-
- );
+ redirect("/login");
}
diff --git a/src/components/admin/.gitkeep b/src/components/admin/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/src/components/layout/.gitkeep b/src/components/layout/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/src/components/layout/app-header.tsx b/src/components/layout/app-header.tsx
new file mode 100644
index 0000000..d0e0d20
--- /dev/null
+++ b/src/components/layout/app-header.tsx
@@ -0,0 +1,31 @@
+import { SidebarTrigger } from "@/components/ui/sidebar";
+import type { SessionUser } from "@/features/auth/types";
+import type { HouseholdSummary } from "@/features/households/types";
+import { tr } from "@/i18n/tr";
+import { HouseholdSwitcher } from "./household-switcher";
+import { QuickAddButton } from "./quick-add";
+import { ThemeToggle } from "./theme-toggle";
+import { UserMenu } from "./user-menu";
+
+/** Üst bar: menü düğmesi (masaüstü), hane seçici, hızlı ekle, tema ve kullanıcı menüsü. */
+export function AppHeader({
+ user,
+ households,
+ version,
+}: {
+ user: SessionUser;
+ households: HouseholdSummary[];
+ version: string;
+}) {
+ return (
+
+ );
+}
diff --git a/src/components/layout/app-sidebar.tsx b/src/components/layout/app-sidebar.tsx
new file mode 100644
index 0000000..4a4982e
--- /dev/null
+++ b/src/components/layout/app-sidebar.tsx
@@ -0,0 +1,103 @@
+"use client";
+
+import Link from "next/link";
+import { usePathname } from "next/navigation";
+import { Logo } from "@/components/brand/logo";
+import {
+ Sidebar,
+ SidebarContent,
+ SidebarFooter,
+ SidebarGroup,
+ SidebarGroupContent,
+ SidebarGroupLabel,
+ SidebarHeader,
+ SidebarMenu,
+ SidebarMenuBadge,
+ SidebarMenuButton,
+ SidebarMenuItem,
+ SidebarRail,
+ useSidebar,
+} from "@/components/ui/sidebar";
+import { tr } from "@/i18n/tr";
+import { ADMIN_NAV, MAIN_NAV, isActivePath, isAvailable, type NavItem } from "./nav-config";
+
+function NavList({ items, pathname }: { items: NavItem[]; pathname: string }) {
+ const { setOpenMobile } = useSidebar();
+ return (
+
+ {items.map((item) => {
+ const Icon = item.icon;
+ if (!isAvailable(item)) {
+ return (
+
+
+
+ {item.label}
+
+
+ {tr.common.comingSoon}
+
+
+ );
+ }
+ const active = isActivePath(pathname, item.href);
+ return (
+
+
+ setOpenMobile(false)}
+ >
+
+ {item.label}
+
+
+
+ );
+ })}
+
+ );
+}
+
+/** Masaüstünde sabit yan menü; mobilde "Daha fazla" ile açılan Sheet. */
+export function AppSidebar({ isAdmin, version }: { isAdmin: boolean; version: string }) {
+ const pathname = usePathname();
+ return (
+
+
+
+
+
+
+
+
+ {tr.nav.main}
+
+
+
+
+ {isAdmin ? (
+
+ {tr.nav.admin}
+
+
+
+
+ ) : null}
+
+
+ {tr.app.name} · {tr.app.version} {version}
+
+
+
+ );
+}
diff --git a/src/components/layout/bottom-nav.tsx b/src/components/layout/bottom-nav.tsx
new file mode 100644
index 0000000..ce8bc15
--- /dev/null
+++ b/src/components/layout/bottom-nav.tsx
@@ -0,0 +1,80 @@
+"use client";
+
+import { Menu, Plus } from "lucide-react";
+import Link from "next/link";
+import { usePathname } from "next/navigation";
+import { useSidebar } from "@/components/ui/sidebar";
+import { tr } from "@/i18n/tr";
+import { cn } from "@/lib/utils";
+import { BOTTOM_NAV, isActivePath, isAvailable, type NavItem } from "./nav-config";
+import { useQuickAdd } from "./quick-add";
+
+const itemClass =
+ "flex min-h-14 flex-1 flex-col items-center justify-center gap-0.5 text-[0.7rem] font-medium outline-none focus-visible:bg-muted";
+
+function BottomNavLink({ item, pathname }: { item: NavItem; pathname: string }) {
+ const Icon = item.icon;
+ if (!isAvailable(item)) {
+ return (
+
+
+ {item.label}
+
+ );
+ }
+ const active = isActivePath(pathname, item.href);
+ return (
+
+
+ {item.label}
+
+ );
+}
+
+/** Mobil alt gezinme çubuğu (md altı). */
+export function BottomNav() {
+ const pathname = usePathname();
+ const { openQuickAdd } = useQuickAdd();
+ const { setOpenMobile } = useSidebar();
+ return (
+
+ );
+}
diff --git a/src/components/layout/household-switcher.tsx b/src/components/layout/household-switcher.tsx
new file mode 100644
index 0000000..733738f
--- /dev/null
+++ b/src/components/layout/household-switcher.tsx
@@ -0,0 +1,37 @@
+"use client";
+
+import { House } from "lucide-react";
+import { useState } from "react";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import type { HouseholdSummary } from "@/features/households/types";
+import { tr } from "@/i18n/tr";
+
+/** Üst bar aktif hane seçici. FAZ 5: seçim sunucuda (cookie) saklanacak. */
+export function HouseholdSwitcher({ households }: { households: HouseholdSummary[] }) {
+ const [value, setValue] = useState(households[0]?.id);
+ const current = households.find((h) => h.id === value);
+ if (households.length === 0) return null;
+ return (
+
+ );
+}
diff --git a/src/components/layout/nav-config.ts b/src/components/layout/nav-config.ts
new file mode 100644
index 0000000..1ede442
--- /dev/null
+++ b/src/components/layout/nav-config.ts
@@ -0,0 +1,59 @@
+import {
+ ArrowLeftRight,
+ CalendarClock,
+ ChartColumn,
+ Coins,
+ House,
+ LayoutDashboard,
+ Package,
+ Settings,
+ ShoppingCart,
+ Target,
+ UserCog,
+ Users,
+ Wallet,
+ type LucideIcon,
+} from "lucide-react";
+import { tr } from "@/i18n/tr";
+
+export type NavItem = {
+ href: string;
+ label: string;
+ icon: LucideIcon;
+ /** Sayfanın ekleneceği faz; mevcut fazdan büyükse "Yakında" olarak pasif gösterilir. */
+ phase: number;
+};
+
+/** Şu an tamamlanmış en yüksek faz — sayfalar eklendikçe artar. */
+export const CURRENT_PHASE = 2;
+
+export const isAvailable = (item: NavItem) => item.phase <= CURRENT_PHASE;
+
+export const MAIN_NAV: NavItem[] = [
+ { href: "/dashboard", label: tr.nav.dashboard, icon: LayoutDashboard, phase: 2 },
+ { href: "/islemler", label: tr.nav.transactions, icon: ArrowLeftRight, phase: 6 },
+ { href: "/hesaplar", label: tr.nav.accounts, icon: Wallet, phase: 6 },
+ { href: "/planli", label: tr.nav.planned, icon: CalendarClock, phase: 7 },
+ { href: "/cari", label: tr.nav.contacts, icon: Users, phase: 8 },
+ { href: "/birikim", label: tr.nav.savings, icon: Coins, phase: 9 },
+ { href: "/butce", label: tr.nav.budget, icon: Target, phase: 10 },
+ { href: "/stok", label: tr.nav.stock, icon: Package, phase: 11 },
+ { href: "/alisveris", label: tr.nav.shopping, icon: ShoppingCart, phase: 13 },
+ { href: "/raporlar", label: tr.nav.reports, icon: ChartColumn, phase: 14 },
+ { href: "/ayarlar", label: tr.nav.settings, icon: Settings, phase: 5 },
+];
+
+export const ADMIN_NAV: NavItem[] = [
+ { href: "/admin/kullanicilar", label: tr.nav.adminUsers, icon: UserCog, phase: 2 },
+ { href: "/admin/haneler", label: tr.nav.adminHouseholds, icon: House, phase: 5 },
+];
+
+/** Mobil alt menü: ortada Hızlı ekle, sağda "Daha fazla" (yan menüyü açar). */
+export const BOTTOM_NAV = {
+ left: [MAIN_NAV[0]!, MAIN_NAV[1]!],
+ right: [MAIN_NAV[6]!],
+};
+
+export function isActivePath(pathname: string, href: string) {
+ return pathname === href || pathname.startsWith(`${href}/`);
+}
diff --git a/src/components/layout/quick-add.tsx b/src/components/layout/quick-add.tsx
new file mode 100644
index 0000000..1b8aa46
--- /dev/null
+++ b/src/components/layout/quick-add.tsx
@@ -0,0 +1,102 @@
+"use client";
+
+import { ArrowDownLeft, ArrowLeftRight, ArrowUpRight, Plus } from "lucide-react";
+import { createContext, use, useState } from "react";
+import { toast } from "sonner";
+import { ResponsiveDialog } from "@/components/responsive-dialog";
+import { Button } from "@/components/ui/button";
+import { tr } from "@/i18n/tr";
+import { cn } from "@/lib/utils";
+
+type QuickAddKind = "expense" | "income" | "transfer";
+
+const QuickAddContext = createContext<{ openQuickAdd: () => void } | null>(null);
+
+export function useQuickAdd() {
+ const ctx = use(QuickAddContext);
+ if (!ctx) throw new Error("useQuickAdd, QuickAddProvider içinde kullanılmalı");
+ return ctx;
+}
+
+const CHOICES: {
+ kind: QuickAddKind;
+ label: string;
+ hint: string;
+ icon: typeof Plus;
+ tone: string;
+}[] = [
+ {
+ kind: "expense",
+ label: tr.quickAdd.expense,
+ hint: tr.quickAdd.expenseHint,
+ icon: ArrowUpRight,
+ tone: "text-expense bg-expense/10",
+ },
+ {
+ kind: "income",
+ label: tr.quickAdd.income,
+ hint: tr.quickAdd.incomeHint,
+ icon: ArrowDownLeft,
+ tone: "text-income bg-income/10",
+ },
+ {
+ kind: "transfer",
+ label: tr.quickAdd.transfer,
+ hint: tr.quickAdd.transferHint,
+ icon: ArrowLeftRight,
+ tone: "text-budget bg-budget/10",
+ },
+];
+
+/** Hızlı ekle (gider / gelir / transfer) — her ekrandan açılabilir. */
+export function QuickAddProvider({ children }: { children: React.ReactNode }) {
+ const [open, setOpen] = useState(false);
+
+ function choose(kind: QuickAddKind) {
+ setOpen(false);
+ // FAZ 6: ilgili işlem formu açılacak.
+ toast.info(`${CHOICES.find((c) => c.kind === kind)?.label}: ${tr.quickAdd.notYet}`);
+ }
+
+ return (
+ setOpen(true) }}>
+ {children}
+
+
+ {CHOICES.map(({ kind, label, hint, icon: Icon, tone }) => (
+
+ ))}
+
+
+
+ );
+}
+
+/** Masaüstü üst bar düğmesi. */
+export function QuickAddButton({ className }: { className?: string }) {
+ const { openQuickAdd } = useQuickAdd();
+ return (
+
+ );
+}
diff --git a/src/components/layout/theme-toggle.tsx b/src/components/layout/theme-toggle.tsx
new file mode 100644
index 0000000..ec35167
--- /dev/null
+++ b/src/components/layout/theme-toggle.tsx
@@ -0,0 +1,46 @@
+"use client";
+
+import { Monitor, Moon, Sun } from "lucide-react";
+import { useTheme } from "next-themes";
+import { Button } from "@/components/ui/button";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuLabel,
+ DropdownMenuRadioGroup,
+ DropdownMenuRadioItem,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import { tr } from "@/i18n/tr";
+
+const OPTIONS = [
+ { value: "light", label: tr.theme.light, icon: Sun },
+ { value: "dark", label: tr.theme.dark, icon: Moon },
+ { value: "system", label: tr.theme.system, icon: Monitor },
+] as const;
+
+/** Açık / Koyu / Sistem tema seçici. İkon CSS ile değişir (hydration güvenli). */
+export function ThemeToggle() {
+ const { theme, setTheme } = useTheme();
+ return (
+
+
+
+
+
+ {tr.theme.label}
+
+ {OPTIONS.map(({ value, label, icon: Icon }) => (
+
+
+ {label}
+
+ ))}
+
+
+
+ );
+}
diff --git a/src/components/layout/user-menu.tsx b/src/components/layout/user-menu.tsx
new file mode 100644
index 0000000..3987572
--- /dev/null
+++ b/src/components/layout/user-menu.tsx
@@ -0,0 +1,75 @@
+"use client";
+
+import { LogOut } from "lucide-react";
+import { useRouter } from "next/navigation";
+import { toast } from "sonner";
+import { Avatar, AvatarFallback } from "@/components/ui/avatar";
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import type { SessionUser } from "@/features/auth/types";
+import { tr } from "@/i18n/tr";
+
+export function initials(name: string) {
+ return name
+ .split(/\s+/)
+ .filter(Boolean)
+ .slice(0, 2)
+ .map((p) => p.charAt(0).toLocaleUpperCase("tr-TR"))
+ .join("");
+}
+
+/** Kullanıcı menüsü: ad, rol, sürüm ve Çıkış yap. */
+export function UserMenu({ user, version }: { user: SessionUser; version: string }) {
+ const router = useRouter();
+
+ function logout() {
+ // FAZ 3: authClient.signOut() sonrası yönlendirme.
+ toast.success(tr.userMenu.loggedOut);
+ router.replace("/login");
+ }
+
+ return (
+
+
+
+
+
+
+ {user.name}
+ @{user.username}
+
+ {tr.roles[user.role]}
+
+
+
+
+
+ {tr.userMenu.logout}
+
+
+
+ {tr.app.name} · {tr.app.version} {version}
+
+
+
+ );
+}
diff --git a/src/components/page-header.tsx b/src/components/page-header.tsx
new file mode 100644
index 0000000..c3fe65d
--- /dev/null
+++ b/src/components/page-header.tsx
@@ -0,0 +1,20 @@
+/** Sayfa başlığı + açıklama + sağda isteğe bağlı aksiyonlar. */
+export function PageHeader({
+ title,
+ description,
+ actions,
+}: {
+ title: string;
+ description?: string;
+ actions?: React.ReactNode;
+}) {
+ return (
+
+
+
{title}
+ {description ?
{description}
: null}
+
+ {actions ?
{actions}
: null}
+
+ );
+}
diff --git a/src/components/password-input.tsx b/src/components/password-input.tsx
new file mode 100644
index 0000000..cccb94a
--- /dev/null
+++ b/src/components/password-input.tsx
@@ -0,0 +1,33 @@
+"use client";
+
+import { Eye, EyeOff } from "lucide-react";
+import { useState } from "react";
+import {
+ InputGroup,
+ InputGroupAddon,
+ InputGroupButton,
+ InputGroupInput,
+} from "@/components/ui/input-group";
+import { tr } from "@/i18n/tr";
+
+/** Göster/gizle düğmeli şifre alanı. */
+export function PasswordInput(props: Omit, "type">) {
+ const [visible, setVisible] = useState(false);
+ const label = visible ? tr.common.hidePassword : tr.common.showPassword;
+ return (
+
+
+
+ setVisible((v) => !v)}
+ >
+ {visible ? : }
+
+
+
+ );
+}
diff --git a/src/components/providers.tsx b/src/components/providers.tsx
new file mode 100644
index 0000000..204b893
--- /dev/null
+++ b/src/components/providers.tsx
@@ -0,0 +1,17 @@
+"use client";
+
+import { ThemeProvider } from "next-themes";
+import { Toaster } from "@/components/ui/sonner";
+import { TooltipProvider } from "@/components/ui/tooltip";
+
+/** Kök istemci sağlayıcıları: tema (açık/koyu/sistem), tooltip ve bildirimler. */
+export function Providers({ children }: { children: React.ReactNode }) {
+ return (
+
+
+ {children}
+
+
+
+ );
+}
diff --git a/src/components/responsive-dialog.tsx b/src/components/responsive-dialog.tsx
new file mode 100644
index 0000000..adec77b
--- /dev/null
+++ b/src/components/responsive-dialog.tsx
@@ -0,0 +1,74 @@
+"use client";
+
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import {
+ Drawer,
+ DrawerContent,
+ DrawerDescription,
+ DrawerFooter,
+ DrawerHeader,
+ DrawerTitle,
+} from "@/components/ui/drawer";
+import { useIsMobile } from "@/hooks/use-mobile";
+import { cn } from "@/lib/utils";
+
+type Props = {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ title: React.ReactNode;
+ description?: React.ReactNode;
+ children: React.ReactNode;
+ footer?: React.ReactNode;
+ className?: string;
+};
+
+/**
+ * Masaüstünde Dialog, mobilde (neredeyse) tam ekran alttan açılan Drawer.
+ * Formlar bu bileşenle açılır (Bölüm 4: "formlar mobilde tam ekran Sheet/Drawer").
+ */
+export function ResponsiveDialog({
+ open,
+ onOpenChange,
+ title,
+ description,
+ children,
+ footer,
+ className,
+}: Props) {
+ const isMobile = useIsMobile();
+
+ if (isMobile) {
+ return (
+
+
+
+ {title}
+ {description ? {description} : null}
+
+ {children}
+ {footer ? {footer} : null}
+
+
+ );
+ }
+
+ return (
+
+ );
+}
diff --git a/src/features/auth/mock.ts b/src/features/auth/mock.ts
new file mode 100644
index 0000000..07d770d
--- /dev/null
+++ b/src/features/auth/mock.ts
@@ -0,0 +1,13 @@
+import type { SessionUser } from "./types";
+
+/**
+ * FAZ 2 GEÇİCİ: Better Auth bağlanana kadar arayüzü besleyen sahte oturum.
+ * Faz 3'te `requireSession()` (lib/dal.ts) ile değiştirilecek ve bu dosya silinecek.
+ */
+export const MOCK_SESSION_USER: SessionUser = {
+ id: "u_admin",
+ name: "Suphi Çakır",
+ username: "suphi",
+ email: "admin@butcem.local",
+ role: "admin",
+};
diff --git a/src/features/auth/types.ts b/src/features/auth/types.ts
new file mode 100644
index 0000000..70f75fe
--- /dev/null
+++ b/src/features/auth/types.ts
@@ -0,0 +1,11 @@
+import type { SystemRole } from "@/shared/schemas/common";
+
+/** Oturumdaki kullanıcının arayüzde ihtiyaç duyulan özeti. */
+export type SessionUser = {
+ id: string;
+ name: string;
+ username: string;
+ email?: string;
+ role: SystemRole;
+ image?: string;
+};
diff --git a/src/features/dashboard/components/summary-cards-skeleton.tsx b/src/features/dashboard/components/summary-cards-skeleton.tsx
new file mode 100644
index 0000000..4b354b9
--- /dev/null
+++ b/src/features/dashboard/components/summary-cards-skeleton.tsx
@@ -0,0 +1,16 @@
+import { Skeleton } from "@/components/ui/skeleton";
+import { tr } from "@/i18n/tr";
+
+export function SummaryCardsSkeleton() {
+ return (
+
+ {Array.from({ length: 6 }, (_, i) => (
+
+ ))}
+
+ );
+}
diff --git a/src/features/households/mock.ts b/src/features/households/mock.ts
new file mode 100644
index 0000000..04e0c4b
--- /dev/null
+++ b/src/features/households/mock.ts
@@ -0,0 +1,7 @@
+import type { HouseholdSummary } from "./types";
+
+/** FAZ 2 GEÇİCİ: Faz 5'te kullanıcının üye olduğu hanelerle değiştirilecek. */
+export const MOCK_HOUSEHOLDS: HouseholdSummary[] = [
+ { id: "h_cakir", name: "Çakır Ailesi", role: "owner" },
+ { id: "h_anne", name: "Annemler", role: "viewer" },
+];
diff --git a/src/features/households/types.ts b/src/features/households/types.ts
new file mode 100644
index 0000000..b8d911a
--- /dev/null
+++ b/src/features/households/types.ts
@@ -0,0 +1,8 @@
+export const HOUSEHOLD_ROLES = ["owner", "member", "viewer"] as const;
+export type HouseholdRole = (typeof HOUSEHOLD_ROLES)[number];
+
+export type HouseholdSummary = {
+ id: string;
+ name: string;
+ role: HouseholdRole;
+};