feat: simplify to single-wishlist instance
Some checks failed
Build and Push Docker Image / build-and-push (push) Has been cancelled

- Home page redirects guests directly to the single wishlist slug
- Admin shows settings + single wishlist header + items only
- Removed multi-wishlist create/delete/reorder UI

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Adriano Belisario
2026-05-03 20:57:00 +00:00
parent cac2c223dd
commit 2726be337e
2 changed files with 366 additions and 218 deletions

View File

@@ -3,13 +3,22 @@
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import AdminGuard from '@/components/admin-guard';
import { authApi, wishlistsApi, itemsApi, settingsApi, type Wishlist, type Settings } from '@/lib/api';
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 StatsGrid from '@/components/admin/StatsGrid';
import SettingsSection from '@/components/admin/SettingsSection';
import WishlistCard from '@/components/admin/WishlistCard';
import CreateWishlistModal from '@/components/admin/CreateWishlistModal';
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 (
@@ -21,53 +30,154 @@ export default function AdminPage() {
function AdminPageContent() {
const router = useRouter();
const [wishlists, setWishlists] = useState<Wishlist[]>([]);
const [itemCounts, setItemCounts] = useState<Record<string, number>>({});
// 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: 'Browse and explore available wishlists',
homepageSubtext: '',
});
// Wishlist edit state
const [isEditingWishlist, setIsEditingWishlist] = useState(false);
const [editForm, setEditForm] = useState({
name: '',
slug: '',
description: '',
preferences: '',
imageUrl: '',
isPublic: true,
});
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('/');
};
const [showCreateModal, setShowCreateModal] = useState(false);
const [createError, setCreateError] = useState('');
useEffect(() => {
fetchWishlists();
fetchSettings();
fetchData();
}, []);
const fetchSettings = async () => {
const fetchData = async () => {
try {
const data = await settingsApi.getSettings();
setSettings(data);
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 settings:', error);
console.error('Failed to fetch data:', error);
} finally {
setIsLoading(false);
}
};
const fetchWishlists = async () => {
try {
const data = await wishlistsApi.getAll();
setWishlists(data);
const refreshItems = async () => {
if (!wishlist) return;
const itemsData = await itemsApi.getAll(wishlist.id);
setItems(itemsData.sort((a, b) => a.sortOrder - b.sortOrder));
};
// Fetch item counts for each wishlist
const counts: Record<string, number> = {};
await Promise.all(
data.map(async (w) => {
const items = await itemsApi.getAll(w.id);
counts[w.id] = items.length;
})
);
setItemCounts(counts);
} catch (error) {
console.error('Failed to fetch wishlists:', error);
} finally {
setIsLoading(false);
// 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,
});
setEditError('');
setIsEditingWishlist(true);
};
const handleUpdateWishlist = async (e: React.FormEvent) => {
e.preventDefault();
if (!wishlist) return;
setEditError('');
try {
const updated = await wishlistsApi.update(wishlist.id, editForm);
setWishlist(updated);
setIsEditingWishlist(false);
} catch (error: any) {
setEditError(error.message || 'Failed to update wishlist');
}
};
// 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');
}
};
@@ -76,172 +186,251 @@ function AdminPageContent() {
setSettings(updatedSettings);
};
const handleCreateWishlist = async (data: any) => {
setCreateError('');
try {
await wishlistsApi.create(data);
setShowCreateModal(false);
fetchWishlists();
} catch (error: any) {
setCreateError(error.message || 'Failed to create wishlist');
throw error;
}
};
const handleUpdateWishlist = async (id: string, data: Partial<Wishlist>) => {
await wishlistsApi.update(id, data);
fetchWishlists();
};
const handleDeleteWishlist = async (id: string) => {
if (!confirm('Are you sure you want to delete this wishlist?')) return;
try {
await wishlistsApi.delete(id);
fetchWishlists();
} catch (error) {
alert('Failed to delete wishlist');
}
};
const handleMoveWishlistUp = async (wishlistId: string) => {
const currentIndex = wishlists.findIndex((w) => w.id === wishlistId);
if (currentIndex <= 0) return;
try {
await wishlistsApi.reorder(wishlistId, currentIndex - 1);
await fetchWishlists();
} catch (error: any) {
alert(`Error: ${error?.message || 'Failed to reorder wishlist'}`);
}
};
const handleMoveWishlistDown = async (wishlistId: string) => {
const currentIndex = wishlists.findIndex((w) => w.id === wishlistId);
if (currentIndex === -1 || currentIndex === wishlists.length - 1) return;
try {
await wishlistsApi.reorder(wishlistId, currentIndex + 1);
await fetchWishlists();
} catch (error: any) {
alert(`Error: ${error?.message || 'Failed to reorder wishlist'}`);
}
};
const stats = {
totalWishlists: wishlists.length,
publicWishlists: wishlists.filter((w) => w.isPublic).length,
totalItems: Object.values(itemCounts).reduce((sum, count) => sum + count, 0),
};
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="Manage your wishlists and items"
actions={
<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>
) : (
<>
<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>
</>
}
/>
{/* Settings Section */}
<SettingsSection settings={settings} onUpdate={handleUpdateSettings} />
<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">Loading...</p>
</div>
) : (
<>
{/* Stats Grid */}
<StatsGrid stats={stats} />
{/* Wishlist Info Section */}
{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">
{isEditingWishlist ? (
<form onSubmit={handleUpdateWishlist} className="space-y-4">
<h2 className="text-xl font-bold text-gray-900 dark:text-white mb-4">
Editar Lista
</h2>
{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>
)}
<ImageUpload
currentImageUrl={editForm.imageUrl}
onImageChange={(url) =>
setEditForm((prev) => ({ ...prev, imageUrl: url }))
}
onUploadStateChange={setIsWishlistImageUploading}
type="wishlist"
label="Imagem da Lista"
/>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Nome *
</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>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Descrição
</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>
<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="flex items-start gap-4">
{wishlist.imageUrl && (
<img
src={wishlist.imageUrl}
alt={wishlist.name}
className="w-20 h-20 object-cover rounded border border-gray-200 dark:border-gray-600 flex-shrink-0"
/>
)}
<div className="flex-1">
<h2 className="text-xl font-bold text-gray-900 dark:text-white">
{wishlist.name}
</h2>
{wishlist.description && (
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
{wishlist.description}
</p>
)}
<p className="text-sm text-gray-500 dark:text-gray-500 mt-1">
/{wishlist.slug}
</p>
</div>
<button
onClick={startEditingWishlist}
className="flex-shrink-0 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>
</div>
)}
{/* Settings Section */}
<SettingsSection settings={settings} onUpdate={handleUpdateSettings} />
{/* Wishlists Section */}
{/* 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">
Your Wishlists
Itens ({items.length})
</h2>
<button
onClick={() => setShowCreateModal(true)}
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"
>
+ Create Wishlist
+ Adicionar Item
</button>
</div>
{wishlists.length === 0 ? (
{/* 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">
No wishlists yet
Nenhum item ainda
</p>
<button
onClick={() => setShowCreateModal(true)}
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"
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"
>
Create Your First Wishlist
Adicionar Primeiro Item
</button>
</div>
) : (
<div className="space-y-3">
{wishlists.map((wishlist, index) => (
<WishlistCard
key={wishlist.id}
wishlist={wishlist}
itemCount={itemCounts[wishlist.id] || 0}
onUpdate={handleUpdateWishlist}
onDelete={handleDeleteWishlist}
onMoveUp={handleMoveWishlistUp}
onMoveDown={handleMoveWishlistDown}
isFirst={index === 0}
isLast={index === wishlists.length - 1}
onItemsChange={fetchWishlists}
/>
{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>
</>
)}
</div>
</div>
)}
{/* Create Modal */}
<CreateWishlistModal
isOpen={showCreateModal}
onClose={() => {
setShowCreateModal(false);
setCreateError('');
}}
onCreate={handleCreateWishlist}
error={createError}
/>
{!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>
);
}