first commit
This commit is contained in:
389
app/admin/users/UsersClient.tsx
Normal file
389
app/admin/users/UsersClient.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
224
app/admin/users/actions.ts
Normal file
224
app/admin/users/actions.ts
Normal file
@@ -0,0 +1,224 @@
|
||||
'use server'
|
||||
|
||||
import { revalidatePath } from 'next/cache'
|
||||
import { redirect } from 'next/navigation'
|
||||
import { getAccessToken, refreshAccessToken } from '@/app/auth/actions'
|
||||
|
||||
const API_BASE = process.env.API_BASE_URL ?? 'http://localhost:8080'
|
||||
|
||||
async function getToken(): Promise<string> {
|
||||
const token = await getAccessToken()
|
||||
if (token) return token
|
||||
const fresh = await refreshAccessToken()
|
||||
if (fresh) return fresh
|
||||
redirect('/auth/login?from=/admin/users')
|
||||
}
|
||||
|
||||
/** API'nin döndürdüğü hata yanıtından okunabilir bir mesaj çıkarır. */
|
||||
function extractError(data: Record<string, unknown>, fallback: string): string {
|
||||
if (typeof data?.error === 'string' && data.error) return data.error
|
||||
if (typeof data?.message === 'string' && data.message) return data.message
|
||||
if (typeof data?.detail === 'string' && data.detail) return data.detail
|
||||
if (data?.errors && typeof data.errors === 'object') {
|
||||
const msgs = Object.values(data.errors as Record<string, string>).filter(Boolean)
|
||||
if (msgs.length > 0) return msgs.join(', ')
|
||||
}
|
||||
console.error('[API Error]', JSON.stringify(data))
|
||||
return fallback
|
||||
}
|
||||
|
||||
export type User = {
|
||||
id: number
|
||||
username: string
|
||||
email: string
|
||||
email_verified: boolean
|
||||
is_active: boolean
|
||||
is_admin: boolean
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export type UsersResponse = {
|
||||
items: User[]
|
||||
meta: { page: number; limit: number; total: number }
|
||||
}
|
||||
|
||||
export type UserFormState = {
|
||||
error?: string
|
||||
success?: boolean
|
||||
user?: User
|
||||
deletedId?: number
|
||||
}
|
||||
|
||||
export async function getUsers(page = 1, limit = 10): Promise<UsersResponse> {
|
||||
let token = await getToken()
|
||||
const url = `${API_BASE}/api/v1/admin/users?page=${page}&limit=${limit}`
|
||||
|
||||
const doFetch = () =>
|
||||
fetch(url, { headers: { Authorization: `Bearer ${token}` }, cache: 'no-store' })
|
||||
|
||||
let res: Response
|
||||
try {
|
||||
res = await doFetch()
|
||||
} catch (error) {
|
||||
console.error('[getUsers] fetch failed', error)
|
||||
throw new Error('API sunucusuna bağlanılamadı. Backend çalışıyor mu kontrol edin.')
|
||||
}
|
||||
|
||||
// Token süresi dolmuşsa yenile ve bir kez daha dene
|
||||
if (res.status === 401) {
|
||||
const fresh = await refreshAccessToken()
|
||||
if (!fresh) throw new Error('Oturum süresi doldu')
|
||||
token = fresh
|
||||
try {
|
||||
res = await doFetch()
|
||||
} catch (error) {
|
||||
console.error('[getUsers] fetch failed after refresh', error)
|
||||
throw new Error('API sunucusuna bağlanılamadı. Backend çalışıyor mu kontrol edin.')
|
||||
}
|
||||
}
|
||||
|
||||
// Rate limit aşıldıysa kısa aralıkla sınırlı sayıda tekrar dene
|
||||
if (res.status === 429) {
|
||||
for (const waitMs of [500, 1000, 1500]) {
|
||||
await new Promise((r) => setTimeout(r, waitMs))
|
||||
res = await doFetch()
|
||||
if (res.ok) break
|
||||
if (res.status !== 429) break
|
||||
}
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
console.error(`[getUsers] API error: ${res.status} ${res.statusText}`)
|
||||
if (res.status === 429) {
|
||||
throw new Error('Çok fazla istek gönderildi, lütfen birkaç saniye sonra tekrar deneyin (429)')
|
||||
}
|
||||
throw new Error(`Kullanıcılar alınamadı (${res.status})`)
|
||||
}
|
||||
return res.json()
|
||||
}
|
||||
|
||||
export async function createUser(
|
||||
_prev: UserFormState,
|
||||
formData: FormData
|
||||
): Promise<UserFormState> {
|
||||
'use server'
|
||||
try {
|
||||
const token = await getToken()
|
||||
const body = {
|
||||
username: formData.get('username') as string,
|
||||
email: formData.get('email') as string,
|
||||
password: formData.get('password') as string,
|
||||
confirm_password: formData.get('confirm_password') as string,
|
||||
is_active: formData.get('is_active') === 'true',
|
||||
is_admin: formData.get('is_admin') === 'true',
|
||||
}
|
||||
const res = await fetch(`${API_BASE}/api/v1/admin/users`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => ({}))
|
||||
return { error: extractError(data, 'Kullanıcı oluşturulamadı') }
|
||||
}
|
||||
revalidatePath('/admin/users')
|
||||
const created = (await res.json()) as User
|
||||
return { success: true, user: created }
|
||||
} catch (e) {
|
||||
console.error('[createUser]', e)
|
||||
return { error: 'Sunucu hatası' }
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateUser(
|
||||
id: number,
|
||||
_prev: UserFormState,
|
||||
formData: FormData
|
||||
): Promise<UserFormState> {
|
||||
'use server'
|
||||
try {
|
||||
const token = await getToken()
|
||||
const body = {
|
||||
username: formData.get('username') as string,
|
||||
email: formData.get('email') as string,
|
||||
password: formData.get('password') as string,
|
||||
confirm_password: formData.get('confirm_password') as string,
|
||||
is_active: formData.get('is_active') === 'true',
|
||||
is_admin: formData.get('is_admin') === 'true',
|
||||
}
|
||||
const res = await fetch(`${API_BASE}/api/v1/admin/users/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => ({}))
|
||||
return { error: extractError(data, 'Kullanıcı güncellenemedi') }
|
||||
}
|
||||
revalidatePath('/admin/users')
|
||||
const updated = (await res.json()) as User
|
||||
return { success: true, user: updated }
|
||||
} catch (e) {
|
||||
console.error('[updateUser]', e)
|
||||
return { error: 'Sunucu hatası' }
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteUser(
|
||||
_prev: UserFormState,
|
||||
formData: FormData
|
||||
): Promise<UserFormState> {
|
||||
'use server'
|
||||
try {
|
||||
const token = await getToken()
|
||||
const id = formData.get('id') as string
|
||||
const res = await fetch(`${API_BASE}/api/v1/admin/users/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
})
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => ({}))
|
||||
return { error: extractError(data, 'Kullanıcı silinemedi') }
|
||||
}
|
||||
revalidatePath('/admin/users')
|
||||
return { success: true, deletedId: Number(id) }
|
||||
} catch (e) {
|
||||
console.error('[deleteUser]', e)
|
||||
return { error: 'Sunucu hatası' }
|
||||
}
|
||||
}
|
||||
|
||||
export async function changeUserStatus(
|
||||
id: number,
|
||||
isActive: boolean
|
||||
): Promise<UserFormState> {
|
||||
'use server'
|
||||
try {
|
||||
const token = await getToken()
|
||||
const res = await fetch(`${API_BASE}/api/v1/admin/users/${id}/status`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ is_active: isActive }),
|
||||
})
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => ({}))
|
||||
return { error: extractError(data, 'Durum güncellenemedi') }
|
||||
}
|
||||
revalidatePath('/admin/users')
|
||||
const updated = (await res.json()) as User
|
||||
return { success: true, user: updated }
|
||||
} catch (e) {
|
||||
console.error('[changeUserStatus]', e)
|
||||
return { error: 'Sunucu hatası' }
|
||||
}
|
||||
}
|
||||
32
app/admin/users/error.tsx
Normal file
32
app/admin/users/error.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect } from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { AlertTriangle } from 'lucide-react'
|
||||
|
||||
export default function UsersError({
|
||||
error,
|
||||
unstable_retry,
|
||||
}: {
|
||||
error: Error & { digest?: string }
|
||||
unstable_retry: () => void
|
||||
}) {
|
||||
useEffect(() => {
|
||||
console.error('[UsersPage Error]', error)
|
||||
}, [error])
|
||||
|
||||
return (
|
||||
<div className="flex h-64 flex-col items-center justify-center gap-4 p-6 text-center">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-destructive/10">
|
||||
<AlertTriangle className="size-6 text-destructive" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="font-semibold">Kullanıcılar yüklenemedi</h2>
|
||||
<p className="mt-1 text-sm text-muted-foreground">{error.message}</p>
|
||||
</div>
|
||||
<Button variant="outline" onClick={unstable_retry}>
|
||||
Yeniden Dene
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
7
app/admin/users/page.tsx
Normal file
7
app/admin/users/page.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import { getUsers } from './actions'
|
||||
import UsersClient from './UsersClient'
|
||||
|
||||
export default async function UsersPage() {
|
||||
const data = await getUsers(1, 10)
|
||||
return <UsersClient initialData={data} />
|
||||
}
|
||||
Reference in New Issue
Block a user