feat(admin): kullanıcı yönetimi tablosu ve diyalogları (mock)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-05 01:35:10 +03:00
parent 1cf9fd9d8c
commit 3b94bceaf0
9 changed files with 1282 additions and 0 deletions
@@ -0,0 +1,219 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { Loader2 } from "lucide-react";
import { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { PasswordInput } from "@/components/password-input";
import { ResponsiveDialog } from "@/components/responsive-dialog";
import {
AlertDialog,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { Field, FieldDescription, FieldError, FieldGroup, FieldLabel } from "@/components/ui/field";
import { Textarea } from "@/components/ui/textarea";
import { tr } from "@/i18n/tr";
import type { ActionResult } from "@/lib/action-result";
import {
banUserSchema,
setPasswordSchema,
type BanUserInput,
type SetPasswordInput,
} from "@/shared/schemas/user";
import type { AdminUser } from "../types";
const t = tr.adminUsers;
type DialogProps<T> = {
user: AdminUser | undefined;
open: boolean;
onOpenChange: (open: boolean) => void;
onConfirm: (user: AdminUser, values: T) => Promise<ActionResult>;
};
export function SetPasswordDialog({
user,
open,
onOpenChange,
onConfirm,
}: DialogProps<{ password: string }>) {
const form = useForm<SetPasswordInput>({
resolver: zodResolver(setPasswordSchema),
defaultValues: { password: "" },
});
useEffect(() => {
if (open) form.reset({ password: "" });
}, [open, form]);
async function onSubmit(values: SetPasswordInput) {
if (!user) return;
const result = await onConfirm(user, values);
if (result.ok) onOpenChange(false);
else form.setError("root", { message: result.error });
}
return (
<ResponsiveDialog
open={open}
onOpenChange={onOpenChange}
title={t.password.title}
description={user ? t.password.description(user.name) : undefined}
footer={
<>
<Button variant="outline" type="button" onClick={() => onOpenChange(false)}>
{tr.common.cancel}
</Button>
<Button type="submit" form="password-form" disabled={form.formState.isSubmitting}>
{form.formState.isSubmitting ? <Loader2 className="animate-spin" /> : null}
{t.password.confirm}
</Button>
</>
}
>
<form id="password-form" onSubmit={form.handleSubmit(onSubmit)} noValidate>
<FieldGroup>
<Controller
name="password"
control={form.control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel htmlFor="new-password">{t.password.newPassword}</FieldLabel>
<PasswordInput
{...field}
id="new-password"
autoComplete="new-password"
aria-invalid={fieldState.invalid}
aria-describedby="new-password-hint"
/>
<FieldDescription id="new-password-hint">{t.form.passwordHint}</FieldDescription>
{fieldState.invalid ? <FieldError errors={[fieldState.error]} /> : null}
</Field>
)}
/>
{form.formState.errors.root ? (
<FieldError>{form.formState.errors.root.message}</FieldError>
) : null}
</FieldGroup>
</form>
</ResponsiveDialog>
);
}
export function BanUserDialog({
user,
open,
onOpenChange,
onConfirm,
}: DialogProps<{ reason?: string }>) {
const form = useForm<BanUserInput, unknown, { reason?: string }>({
resolver: zodResolver(banUserSchema),
defaultValues: { reason: "" },
});
useEffect(() => {
if (open) form.reset({ reason: "" });
}, [open, form]);
async function onSubmit(values: { reason?: string }) {
if (!user) return;
const result = await onConfirm(user, values);
if (result.ok) onOpenChange(false);
else form.setError("root", { message: result.error });
}
return (
<ResponsiveDialog
open={open}
onOpenChange={onOpenChange}
title={t.ban.title}
description={user ? t.ban.description(user.name) : undefined}
footer={
<>
<Button variant="outline" type="button" onClick={() => onOpenChange(false)}>
{tr.common.cancel}
</Button>
<Button
type="submit"
form="ban-form"
variant="destructive"
disabled={form.formState.isSubmitting}
>
{form.formState.isSubmitting ? <Loader2 className="animate-spin" /> : null}
{t.ban.confirm}
</Button>
</>
}
>
<form id="ban-form" onSubmit={form.handleSubmit(onSubmit)} noValidate>
<FieldGroup>
<Controller
name="reason"
control={form.control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel htmlFor="ban-reason">{t.ban.reason}</FieldLabel>
<Textarea
{...field}
value={field.value ?? ""}
id="ban-reason"
rows={3}
aria-invalid={fieldState.invalid}
/>
{fieldState.invalid ? <FieldError errors={[fieldState.error]} /> : null}
</Field>
)}
/>
{form.formState.errors.root ? (
<FieldError>{form.formState.errors.root.message}</FieldError>
) : null}
</FieldGroup>
</form>
</ResponsiveDialog>
);
}
export function DeleteUserDialog({ user, open, onOpenChange, onConfirm }: DialogProps<void>) {
const [pending, setPending] = useState(false);
const [error, setError] = useState<string | null>(null);
function handleOpenChange(next: boolean) {
if (!next) setError(null);
onOpenChange(next);
}
async function confirm() {
if (!user) return;
setPending(true);
const result = await onConfirm(user, undefined);
setPending(false);
if (result.ok) handleOpenChange(false);
else setError(result.error);
}
return (
<AlertDialog open={open} onOpenChange={handleOpenChange}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{t.remove.title}</AlertDialogTitle>
<AlertDialogDescription>
{user ? t.remove.description(user.name) : null}
</AlertDialogDescription>
</AlertDialogHeader>
{error ? <FieldError>{error}</FieldError> : null}
<AlertDialogFooter>
<AlertDialogCancel disabled={pending}>{tr.common.cancel}</AlertDialogCancel>
{/* AlertDialogAction otomatik kapanır; sonuç beklenmesi için normal düğme. */}
<Button variant="destructive" onClick={confirm} disabled={pending}>
{pending ? <Loader2 className="animate-spin" /> : null}
{t.remove.confirm}
</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}