feat(layout): login, uygulama kabuğu, alt menü, hızlı ekle ve dashboard

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-05 01:35:10 +03:00
parent 4dbd2eb441
commit 1cf9fd9d8c
31 changed files with 1061 additions and 14 deletions
+13
View File
@@ -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 (
<div className="relative flex min-h-dvh flex-1 flex-col items-center justify-center bg-gradient-to-b from-primary/5 via-background to-background px-4 py-10">
<div className="absolute top-3 right-3">
<ThemeToggle />
</div>
{children}
</div>
);
}
+81
View File
@@ -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<string | null>(null);
const form = useForm<LoginInput>({
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 (
<form onSubmit={form.handleSubmit(onSubmit)} noValidate aria-busy={submitting}>
<FieldGroup>
<Controller
name="identifier"
control={form.control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel htmlFor="identifier">{tr.auth.identifier}</FieldLabel>
<Input
{...field}
id="identifier"
autoComplete="username"
autoCapitalize="none"
autoCorrect="off"
spellCheck={false}
inputMode="email"
placeholder={tr.auth.identifierPlaceholder}
aria-invalid={fieldState.invalid}
autoFocus
/>
{fieldState.invalid ? <FieldError errors={[fieldState.error]} /> : null}
</Field>
)}
/>
<Controller
name="password"
control={form.control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel htmlFor="password">{tr.auth.password}</FieldLabel>
<PasswordInput
{...field}
id="password"
autoComplete="current-password"
aria-invalid={fieldState.invalid}
/>
{fieldState.invalid ? <FieldError errors={[fieldState.error]} /> : null}
</Field>
)}
/>
{formError ? <FieldError>{formError}</FieldError> : null}
<Button type="submit" size="lg" disabled={submitting} className="w-full">
{submitting ? <Loader2 className="animate-spin" /> : null}
{submitting ? tr.auth.submitting : tr.auth.submit}
</Button>
</FieldGroup>
</form>
);
}
+25
View File
@@ -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 (
<main className="flex w-full max-w-sm flex-col items-center gap-6">
<Logo priority className="scale-125" />
<Card className="w-full">
<CardHeader>
<CardTitle className="text-xl">{tr.auth.loginTitle}</CardTitle>
<CardDescription>{tr.auth.loginDescription}</CardDescription>
</CardHeader>
<CardContent>
<LoginForm />
</CardContent>
</Card>
<p className="text-center text-xs text-muted-foreground">{tr.auth.noSignUp}</p>
</main>
);
}