Files
chadebebe/app/admin/page.tsx
Adriano Belisario 3df9a67b97 feat(admin): merge Site Settings and Wishlist into single Configurações panel
Single-wishlist apps no longer need two separate edit sections.
Both siteTitle/homepageSubtext and wishlist fields now save together
in one action from a unified Configurações card.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 21:10:58 +00:00

468 lines
20 KiB
TypeScript

'use client';
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import AdminGuard from '@/components/admin-guard';
import {
authApi,
wishlistsApi,
itemsApi,
settingsApi,
type Wishlist,
type Item,
type Settings,
} from '@/lib/api';
import Header from '@/components/header';
import Link from 'next/link';
import ItemCard from '@/components/admin/ItemCard';
import ItemForm from '@/components/admin/ItemForm';
import ImageUpload from '@/components/image-upload';
import RichTextEditor from '@/components/RichTextEditor';
export default function AdminPage() {
return (
<AdminGuard>
<AdminPageContent />
</AdminGuard>
);
}
function AdminPageContent() {
const router = useRouter();
// Single wishlist state
const [wishlist, setWishlist] = useState<Wishlist | null>(null);
const [items, setItems] = useState<Item[]>([]);
const [isLoading, setIsLoading] = useState(true);
// Settings
const [settings, setSettings] = useState<Settings>({
siteTitle: 'Wishlist',
homepageSubtext: '',
});
// Unified config edit state
const [isEditingWishlist, setIsEditingWishlist] = useState(false);
const [editForm, setEditForm] = useState({
name: '',
slug: '',
description: '',
preferences: '',
imageUrl: '',
isPublic: true,
siteTitle: '',
homepageSubtext: '',
});
const [editError, setEditError] = useState('');
const [isWishlistImageUploading, setIsWishlistImageUploading] = useState(false);
// Item management state
const [editingItemId, setEditingItemId] = useState<string | null>(null);
const [showAddItemForm, setShowAddItemForm] = useState(false);
const [newItemError, setNewItemError] = useState('');
const logout = async () => {
await authApi.logout();
router.push('/');
};
useEffect(() => {
fetchData();
}, []);
const fetchData = async () => {
try {
const [lists, settingsData] = await Promise.all([
wishlistsApi.getAll(),
settingsApi.getSettings(),
]);
setSettings(settingsData);
if (lists.length > 0) {
const w = lists[0];
setWishlist(w);
const itemsData = await itemsApi.getAll(w.id);
setItems(itemsData.sort((a, b) => a.sortOrder - b.sortOrder));
}
} catch (error) {
console.error('Failed to fetch data:', error);
} finally {
setIsLoading(false);
}
};
const refreshItems = async () => {
if (!wishlist) return;
const itemsData = await itemsApi.getAll(wishlist.id);
setItems(itemsData.sort((a, b) => a.sortOrder - b.sortOrder));
};
// Wishlist edit handlers
const startEditingWishlist = () => {
if (!wishlist) return;
setEditForm({
name: wishlist.name,
slug: wishlist.slug,
description: wishlist.description || '',
preferences: wishlist.preferences || '',
imageUrl: wishlist.imageUrl || '',
isPublic: wishlist.isPublic,
siteTitle: settings.siteTitle,
homepageSubtext: settings.homepageSubtext,
});
setEditError('');
setIsEditingWishlist(true);
};
const handleUpdateWishlist = async (e: React.FormEvent) => {
e.preventDefault();
if (!wishlist) return;
setEditError('');
try {
const { siteTitle, homepageSubtext, ...wishlistFields } = editForm;
const [updated] = await Promise.all([
wishlistsApi.update(wishlist.id, wishlistFields),
settingsApi.updateSettings({ siteTitle, homepageSubtext }),
]);
setWishlist(updated);
setSettings({ siteTitle, homepageSubtext });
setIsEditingWishlist(false);
} catch (error: any) {
setEditError(error.message || 'Failed to update settings');
}
};
// Item handlers
const handleCreateItem = async (itemData: Partial<Item>) => {
if (!wishlist) return;
setNewItemError('');
try {
await itemsApi.create(wishlist.id, itemData);
setShowAddItemForm(false);
await refreshItems();
} catch (error: any) {
setNewItemError(error.message || 'Failed to create item');
throw error;
}
};
const handleUpdateItem = async (itemData: Partial<Item>) => {
if (!editingItemId) return;
try {
await itemsApi.update(editingItemId, itemData);
setEditingItemId(null);
await refreshItems();
} catch (error: any) {
alert(error.message || 'Failed to update item');
throw error;
}
};
const handleDeleteItem = async (itemId: string) => {
if (!confirm('Tem certeza que deseja excluir este item?')) return;
try {
await itemsApi.delete(itemId);
await refreshItems();
} catch {
alert('Failed to delete item');
}
};
const handleMoveItemUp = async (itemId: string) => {
const currentIndex = items.findIndex((item) => item.id === itemId);
if (currentIndex <= 0) return;
try {
await itemsApi.reorder(itemId, currentIndex - 1);
await refreshItems();
} catch (error: any) {
alert(error?.message || 'Failed to reorder item');
}
};
const handleMoveItemDown = async (itemId: string) => {
const currentIndex = items.findIndex((item) => item.id === itemId);
if (currentIndex === -1 || currentIndex === items.length - 1) return;
try {
await itemsApi.reorder(itemId, currentIndex + 1);
await refreshItems();
} catch (error: any) {
alert(error?.message || 'Failed to reorder item');
}
};
const editingItem = items.find((item) => item.id === editingItemId);
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
<Header
title="Dashboard"
subtitle={wishlist ? `${items.length} itens` : undefined}
actions={
<>
<Link
href="/admin/claims"
className="inline-flex items-center px-6 py-3 border-2 border-indigo-600 dark:border-indigo-500 text-base font-semibold rounded-lg text-indigo-600 dark:text-indigo-400 bg-white dark:bg-gray-800 hover:bg-indigo-50 dark:hover:bg-gray-700 transition-all"
>
Reservas
</Link>
<Link
href="/admin/guests"
className="inline-flex items-center px-6 py-3 border-2 border-indigo-600 dark:border-indigo-500 text-base font-semibold rounded-lg text-indigo-600 dark:text-indigo-400 bg-white dark:bg-gray-800 hover:bg-indigo-50 dark:hover:bg-gray-700 transition-all"
>
Convidados
</Link>
<button
onClick={logout}
className="inline-flex items-center px-6 py-3 border-2 border-red-600 dark:border-red-500 text-base font-semibold rounded-lg text-red-600 dark:text-red-400 bg-white dark:bg-gray-800 hover:bg-red-50 dark:hover:bg-red-900/20 transition-all cursor-pointer"
title="Logout"
>
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
</svg>
Logout
</button>
</>
}
/>
<div className="max-w-7xl mx-auto py-8 sm:px-6 lg:px-8">
<div className="px-4 sm:px-0">
{isLoading ? (
<div className="text-center py-12">
<p className="text-gray-600 dark:text-gray-400">Carregando...</p>
</div>
) : (
<>
{/* Unified Settings & Wishlist Config */}
{wishlist && (
<div className="mb-6 bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
<div className="p-5 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between">
<h2 className="text-xl font-bold text-gray-900 dark:text-white">Configurações</h2>
{!isEditingWishlist && (
<button
onClick={startEditingWishlist}
className="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 text-sm cursor-pointer"
>
Editar
</button>
)}
</div>
<div className="p-5">
{isEditingWishlist ? (
<form onSubmit={handleUpdateWishlist} className="space-y-4">
{editError && (
<div className="p-3 bg-red-50 dark:bg-red-900/20 text-red-800 dark:text-red-400 rounded-lg text-base">
{editError}
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Título do site *
</label>
<input
type="text"
required
value={editForm.siteTitle}
onChange={(e) => setEditForm((prev) => ({ ...prev, siteTitle: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Nome da lista *
</label>
<input
type="text"
required
value={editForm.name}
onChange={(e) => setEditForm((prev) => ({ ...prev, name: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Mensagem de boas-vindas
</label>
<textarea
value={editForm.homepageSubtext}
onChange={(e) => setEditForm((prev) => ({ ...prev, homepageSubtext: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white"
rows={2}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Descrição da lista
</label>
<textarea
value={editForm.description}
onChange={(e) => setEditForm((prev) => ({ ...prev, description: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white"
rows={2}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Preferências
</label>
<RichTextEditor
value={editForm.preferences}
onChange={(html) => setEditForm((prev) => ({ ...prev, preferences: html }))}
placeholder="Interesses e preferências gerais..."
/>
</div>
<ImageUpload
currentImageUrl={editForm.imageUrl}
onImageChange={(url) => setEditForm((prev) => ({ ...prev, imageUrl: url }))}
onUploadStateChange={setIsWishlistImageUploading}
type="wishlist"
label="Imagem da Lista"
/>
<div className="flex gap-3 justify-end">
<button
type="button"
onClick={() => setIsEditingWishlist(false)}
className="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 cursor-pointer"
>
Cancelar
</button>
<button
type="submit"
disabled={isWishlistImageUploading}
className="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 disabled:opacity-50 cursor-pointer"
>
{isWishlistImageUploading ? 'Enviando...' : 'Salvar'}
</button>
</div>
</form>
) : (
<div className="space-y-3">
<div className="flex items-start gap-4">
{wishlist.imageUrl && (
<img
src={wishlist.imageUrl}
alt={wishlist.name}
className="w-16 h-16 object-cover rounded border border-gray-200 dark:border-gray-600 flex-shrink-0"
/>
)}
<div className="flex-1 grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-2">
<div>
<p className="text-xs font-medium text-gray-500 dark:text-gray-400">Título do site</p>
<p className="text-sm text-gray-900 dark:text-white">{settings.siteTitle}</p>
</div>
<div>
<p className="text-xs font-medium text-gray-500 dark:text-gray-400">Nome da lista</p>
<p className="text-sm text-gray-900 dark:text-white">{wishlist.name}</p>
</div>
{settings.homepageSubtext && (
<div className="md:col-span-2">
<p className="text-xs font-medium text-gray-500 dark:text-gray-400">Mensagem de boas-vindas</p>
<p className="text-sm text-gray-900 dark:text-white">{settings.homepageSubtext}</p>
</div>
)}
{wishlist.description && (
<div className="md:col-span-2">
<p className="text-xs font-medium text-gray-500 dark:text-gray-400">Descrição</p>
<p className="text-sm text-gray-900 dark:text-white">{wishlist.description}</p>
</div>
)}
</div>
</div>
</div>
)}
</div>
</div>
)}
{/* Items Section */}
{wishlist && (
<div className="mb-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">
Itens ({items.length})
</h2>
<button
onClick={() => {
setShowAddItemForm(true);
setNewItemError('');
}}
className="inline-flex items-center px-6 py-3 border border-transparent text-base font-semibold rounded-lg text-white bg-indigo-600 hover:bg-indigo-700 shadow-md hover:shadow-lg transition-all cursor-pointer"
>
+ Adicionar Item
</button>
</div>
{/* Add Item Form */}
{showAddItemForm && (
<ItemForm
mode="create"
onSubmit={handleCreateItem}
onCancel={() => {
setShowAddItemForm(false);
setNewItemError('');
}}
error={newItemError}
/>
)}
{items.length === 0 && !showAddItemForm ? (
<div className="text-center py-12 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700">
<p className="text-gray-500 dark:text-gray-400 mb-6 text-lg">
Nenhum item ainda
</p>
<button
onClick={() => {
setShowAddItemForm(true);
setNewItemError('');
}}
className="inline-flex items-center px-6 py-3 border border-transparent text-base font-semibold rounded-lg text-white bg-indigo-600 hover:bg-indigo-700 shadow-md hover:shadow-lg transition-all cursor-pointer"
>
Adicionar Primeiro Item
</button>
</div>
) : (
<div className="space-y-3">
{items.map((item, index) => (
<div key={item.id}>
{editingItemId === item.id ? (
<ItemForm
mode="edit"
item={editingItem}
onSubmit={handleUpdateItem}
onCancel={() => setEditingItemId(null)}
/>
) : (
<ItemCard
item={item}
onEdit={() => setEditingItemId(item.id)}
onDelete={() => handleDeleteItem(item.id)}
onMoveUp={() => handleMoveItemUp(item.id)}
onMoveDown={() => handleMoveItemDown(item.id)}
isFirst={index === 0}
isLast={index === items.length - 1}
/>
)}
</div>
))}
</div>
)}
</div>
)}
{!wishlist && !isLoading && (
<div className="text-center py-12 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700">
<p className="text-gray-500 dark:text-gray-400 text-lg">
Nenhuma lista encontrada no banco de dados.
</p>
</div>
)}
</>
)}
</div>
</div>
</div>
);
}