first commit

This commit is contained in:
Beyhan Oğur
2026-04-26 22:15:25 +03:00
commit 9eb7aea821
56 changed files with 20630 additions and 0 deletions

View File

@@ -0,0 +1,389 @@
'use client'
import { useActionState, useCallback, useEffect, useState, useTransition } from 'react'
import Swal, { type SweetAlertIcon } from 'sweetalert2'
import {
createUser,
updateUser,
deleteUser,
changeUserStatus,
type User,
type UserFormState,
type UsersResponse,
} from './actions'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Checkbox } from '@/components/ui/checkbox'
import { Badge } from '@/components/ui/badge'
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
DialogDescription,
} from '@/components/ui/dialog'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table'
import { Loader2, Plus, Pencil, Trash2, AlertTriangle } from 'lucide-react'
type Props = { initialData: UsersResponse }
type ModalMode = 'create' | 'edit' | null
function getSwalThemeOptions() {
const isDark = document.documentElement.classList.contains('dark')
return isDark
? { background: '#111827', color: '#f3f4f6' }
: { background: '#ffffff', color: '#111827' }
}
function showToast(icon: SweetAlertIcon, title: string) {
void Swal.fire({
...getSwalThemeOptions(),
toast: true,
position: 'top-end',
showConfirmButton: false,
timer: 2200,
timerProgressBar: true,
icon,
title,
})
}
/* ─── Field ─────────────────────────────────────────── */
function Field({ id, label, children }: { id?: string; label: string; children: React.ReactNode }) {
return (
<div className="space-y-1.5">
<Label htmlFor={id}>{label}</Label>
{children}
</div>
)
}
/* ─── UserModal ─────────────────────────────────────── */
function UserModal({
open,
mode,
user,
onSaved,
onClose,
}: {
open: boolean
mode: 'create' | 'edit'
user?: User
onSaved: (savedUser: User, mode: 'create' | 'edit') => void
onClose: () => void
}) {
const isEdit = mode === 'edit' && user != null
const boundAction = isEdit ? updateUser.bind(null, user.id) : createUser
const [state, formAction, pending] = useActionState<UserFormState, FormData>(boundAction, {})
useEffect(() => {
if (!state.success) return
if (state.user) onSaved(state.user, mode)
showToast('success', mode === 'edit' ? 'Kullanıcı güncellendi' : 'Kullanıcı oluşturuldu')
onClose()
}, [state.success, state.user, mode, onSaved, onClose])
useEffect(() => {
if (!state.error) return
showToast('error', state.error)
}, [state.error])
return (
<Dialog open={open} onOpenChange={(o) => !o && onClose()}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>{isEdit ? 'Kullanıcıyı Düzenle' : 'Yeni Kullanıcı Ekle'}</DialogTitle>
<DialogDescription>
{isEdit ? 'Kullanıcı bilgilerini güncelleyin.' : 'Yeni bir kullanıcı hesabı oluşturun.'}
</DialogDescription>
</DialogHeader>
<form action={formAction} className="space-y-4">
{state.error && (
<div className="flex items-start gap-2 rounded-lg border border-destructive/30 bg-destructive/10 px-3 py-2.5 text-sm text-destructive">
<AlertTriangle className="mt-0.5 size-4 shrink-0" />
{state.error}
</div>
)}
<Field id="username" label="Kullanıcı Adı">
<Input id="username" name="username" defaultValue={user?.username} required />
</Field>
<Field id="email" label="E-posta">
<Input id="email" name="email" type="email" defaultValue={user?.email} required />
</Field>
<div className="grid grid-cols-2 gap-3">
<Field id="password" label="Şifre">
<Input
id="password"
name="password"
type="password"
required={!isEdit}
placeholder={isEdit ? '••••••' : ''}
/>
</Field>
<Field id="confirm_password" label="Şifre Tekrar">
<Input
id="confirm_password"
name="confirm_password"
type="password"
required={!isEdit}
placeholder={isEdit ? '••••••' : ''}
/>
</Field>
</div>
<div className="flex gap-6">
<div className="flex items-center gap-2">
<Checkbox
id="is_active"
name="is_active"
value="true"
defaultChecked={user?.is_active ?? true}
/>
<Label htmlFor="is_active" className="cursor-pointer font-normal">Aktif</Label>
</div>
<div className="flex items-center gap-2">
<Checkbox
id="is_admin"
name="is_admin"
value="true"
defaultChecked={user?.is_admin ?? false}
/>
<Label htmlFor="is_admin" className="cursor-pointer font-normal">Admin</Label>
</div>
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={onClose}>İptal</Button>
<Button type="submit" disabled={pending}>
{pending && <Loader2 className="size-4 animate-spin" />}
{pending ? 'Kaydediliyor…' : 'Kaydet'}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
)
}
/* ─── UsersClient ────────────────────────────────────── */
export default function UsersClient({ initialData }: Props) {
const [data, setData] = useState<UsersResponse>(initialData)
const [modal, setModal] = useState<ModalMode>(null)
const [selectedUser, setSelectedUser] = useState<User | undefined>()
const [, startTransition] = useTransition()
const openCreate = useCallback(() => {
setSelectedUser(undefined)
setModal('create')
}, [])
const openEdit = useCallback((user: User) => {
setSelectedUser(user)
setModal('edit')
}, [])
const closeModal = useCallback(() => {
setModal(null)
setSelectedUser(undefined)
}, [])
const handleUserSaved = useCallback((savedUser: User, mode: 'create' | 'edit') => {
setData((prev) => {
const existingIndex = prev.items.findIndex((u) => u.id === savedUser.id)
if (mode === 'edit' || existingIndex >= 0) {
return {
...prev,
items: prev.items.map((u) => (u.id === savedUser.id ? savedUser : u)),
}
}
return {
...prev,
items: [savedUser, ...prev.items],
meta: { ...prev.meta, total: prev.meta.total + 1 },
}
})
}, [])
const handleUserDeleted = useCallback((id: number) => {
setData((prev) => {
const nextItems = prev.items.filter((u) => u.id !== id)
const removed = nextItems.length !== prev.items.length
return {
...prev,
items: nextItems,
meta: {
...prev.meta,
total: removed ? Math.max(0, prev.meta.total - 1) : prev.meta.total,
},
}
})
}, [])
const handleDelete = useCallback(async (user: User) => {
const result = await Swal.fire({
...getSwalThemeOptions(),
icon: 'warning',
title: 'Kullanıcı silinsin mi?',
text: `${user.username} kalıcı olarak silinecek.`,
showCancelButton: true,
confirmButtonText: 'Evet, sil',
cancelButtonText: 'İptal',
confirmButtonColor: '#dc2626',
reverseButtons: true,
focusCancel: true,
})
if (!result.isConfirmed) return
const formData = new FormData()
formData.set('id', String(user.id))
const deleted = await deleteUser({}, formData)
if (!deleted.success) {
showToast('error', deleted.error ?? 'Kullanıcı silinemedi')
return
}
handleUserDeleted(deleted.deletedId ?? user.id)
showToast('success', 'Kullanıcı silindi')
}, [handleUserDeleted])
async function handleStatusToggle(user: User) {
const nextStatus = !user.is_active
const statusLabel = nextStatus ? 'aktif' : 'pasif'
const decision = await Swal.fire({
...getSwalThemeOptions(),
icon: 'question',
title: 'Durum değiştirilsin mi?',
text: `${user.username} kullanıcısı ${statusLabel} yapılacak.`,
showCancelButton: true,
confirmButtonText: 'Evet',
cancelButtonText: 'İptal',
reverseButtons: true,
focusCancel: true,
})
if (!decision.isConfirmed) return
startTransition(async () => {
const result = await changeUserStatus(user.id, nextStatus)
if (!result.success || !result.user) {
showToast('error', result.error ?? 'Durum güncellenemedi')
return
}
setData((prev) => ({
...prev,
items: prev.items.map((u) => (u.id === result.user!.id ? result.user! : u)),
}))
showToast('success', `Kullanıcı ${statusLabel} yapıldı`)
})
}
return (
<div className="p-6">
{/* Header */}
<div className="mb-6 flex items-center justify-between">
<div>
<h1 className="text-xl font-semibold">Kullanıcılar</h1>
<p className="text-sm text-muted-foreground">
Toplam <strong className="text-foreground">{data.meta.total}</strong> kullanıcı
</p>
</div>
<Button onClick={openCreate}>
<Plus className="size-4" />
Yeni Kullanıcı
</Button>
</div>
{/* Table */}
<div className="rounded-xl border overflow-hidden">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-16">ID</TableHead>
<TableHead>Kullanıcı Adı</TableHead>
<TableHead>E-posta</TableHead>
<TableHead>Durum</TableHead>
<TableHead>Rol</TableHead>
<TableHead>Kayıt Tarihi</TableHead>
<TableHead className="text-right">İşlemler</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data.items.map((user) => (
<TableRow key={user.id}>
<TableCell className="font-mono text-xs text-muted-foreground">{user.id}</TableCell>
<TableCell className="font-medium">{user.username}</TableCell>
<TableCell className="text-muted-foreground">{user.email}</TableCell>
<TableCell>
<button onClick={() => void handleStatusToggle(user)} className="transition hover:opacity-75">
{user.is_active
? <Badge variant="outline" className="border-green-500/40 bg-green-500/10 text-green-600 dark:text-green-400"> Aktif</Badge>
: <Badge variant="outline" className="border-red-500/40 bg-red-500/10 text-red-600 dark:text-red-400"> Pasif</Badge>
}
</button>
</TableCell>
<TableCell>
{user.is_admin
? <Badge variant="outline" className="border-violet-500/40 bg-violet-500/10 text-violet-600 dark:text-violet-400">Admin</Badge>
: <Badge variant="secondary">Kullanıcı</Badge>
}
</TableCell>
<TableCell className="text-muted-foreground text-sm">
{new Date(user.created_at).toLocaleDateString('tr-TR')}
</TableCell>
<TableCell className="text-right">
<div className="flex items-center justify-end gap-1">
<Button variant="ghost" size="icon-sm" onClick={() => openEdit(user)} aria-label="Düzenle">
<Pencil className="size-3.5" />
</Button>
<Button variant="ghost" size="icon-sm" onClick={() => void handleDelete(user)} aria-label="Sil" className="text-destructive hover:text-destructive">
<Trash2 className="size-3.5" />
</Button>
</div>
</TableCell>
</TableRow>
))}
{data.items.length === 0 && (
<TableRow>
<TableCell colSpan={7} className="h-32 text-center text-muted-foreground">
Henüz kullanıcı yok
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
{/* Pagination */}
{data.meta.total > data.meta.limit && (
<p className="mt-4 text-sm text-muted-foreground">
Sayfa {data.meta.page} / {Math.ceil(data.meta.total / data.meta.limit)}
</p>
)}
{/* Modals */}
<UserModal
open={modal === 'create' || modal === 'edit'}
mode={modal === 'edit' ? 'edit' : 'create'}
user={selectedUser}
onSaved={handleUserSaved}
onClose={closeModal}
/>
</div>
)
}