feat(admin): guests CRUD UI with copy-link
This commit is contained in:
97
app/admin/guests/page.tsx
Normal file
97
app/admin/guests/page.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import AdminGuard from '@/components/admin-guard';
|
||||
import { guestsApi, type Guest } from '@/lib/api';
|
||||
|
||||
export default function AdminGuestsPage() {
|
||||
return (
|
||||
<AdminGuard>
|
||||
<GuestsManager />
|
||||
</AdminGuard>
|
||||
);
|
||||
}
|
||||
|
||||
function GuestsManager() {
|
||||
const [guests, setGuests] = useState<Guest[]>([]);
|
||||
const [name, setName] = useState('');
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [err, setErr] = useState('');
|
||||
|
||||
const load = async () => setGuests(await guestsApi.list());
|
||||
useEffect(() => { load(); }, []);
|
||||
|
||||
const linkFor = (g: Guest) => {
|
||||
const base = typeof window === 'undefined' ? '' : window.location.origin;
|
||||
return `${base}/?usr=${g.id}`;
|
||||
};
|
||||
|
||||
const onCreate = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setErr('');
|
||||
if (!name.trim()) return;
|
||||
setBusy(true);
|
||||
try {
|
||||
await guestsApi.create(name.trim());
|
||||
setName('');
|
||||
await load();
|
||||
} catch (e: any) {
|
||||
setErr(e.message || 'Erro ao criar');
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onRename = async (g: Guest) => {
|
||||
const next = prompt('Novo nome', g.name);
|
||||
if (!next || next.trim() === g.name) return;
|
||||
await guestsApi.rename(g.id, next.trim());
|
||||
await load();
|
||||
};
|
||||
|
||||
const onDelete = async (g: Guest) => {
|
||||
if (!confirm(`Excluir o convidado ${g.name}? As reservas dele(a) ficarão sem dono.`)) return;
|
||||
await guestsApi.delete(g.id);
|
||||
await load();
|
||||
};
|
||||
|
||||
const onCopy = async (g: Guest) => {
|
||||
await navigator.clipboard.writeText(linkFor(g));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-3xl mx-auto p-6 space-y-6">
|
||||
<h1 className="text-2xl font-bold">Convidados</h1>
|
||||
|
||||
<form onSubmit={onCreate} className="flex gap-2">
|
||||
<input
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Nome do convidado"
|
||||
className="flex-1 border rounded px-3 py-2"
|
||||
/>
|
||||
<button disabled={busy} className="bg-indigo-600 text-white rounded px-4 py-2 disabled:opacity-50">
|
||||
{busy ? 'Criando…' : 'Criar link'}
|
||||
</button>
|
||||
</form>
|
||||
{err && <div className="text-red-600 text-sm">{err}</div>}
|
||||
|
||||
<ul className="divide-y border rounded">
|
||||
{guests.map((g) => (
|
||||
<li key={g.id} className="p-3 flex items-center justify-between gap-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium truncate">{g.name}</div>
|
||||
<div className="text-xs text-gray-500 truncate">{linkFor(g)}</div>
|
||||
</div>
|
||||
<div className="flex gap-2 shrink-0">
|
||||
<button onClick={() => onCopy(g)} className="text-sm border rounded px-2 py-1">Copiar link</button>
|
||||
<button onClick={() => onRename(g)} className="text-sm border rounded px-2 py-1">Renomear</button>
|
||||
<button onClick={() => onDelete(g)} className="text-sm border rounded px-2 py-1 text-red-600">Excluir</button>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
{guests.length === 0 && <li className="p-3 text-gray-500 text-sm">Nenhum convidado ainda</li>}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user