Compare commits

...

2 Commits

Author SHA1 Message Date
Adriano Belisario
5548f5000f feat(ui): show price in public view and admin item card; fix quantity badge in claims list
Some checks failed
Build and Push Docker Image / build-and-push (push) Has been cancelled
- Public wishlist page now displays item price (formatted as BRL) below the
  item name when a price is set
- Admin ItemCard shows price and quantity badge so items can be scanned at a
  glance without opening the edit form
- Claims list no longer shows "· 1 un." for single-unit items (the unit count
  is only displayed when item.quantity > 1)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 21:13:45 +00:00
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
7 changed files with 145 additions and 65 deletions

View File

@@ -221,6 +221,11 @@ function PublicWishlistContent() {
<h3 className="text-2xl font-bold text-gray-900 dark:text-white mb-1">
{item.name}
</h3>
{item.price != null && (
<p className="text-lg font-semibold text-indigo-600 dark:text-indigo-400 mb-1">
{new Intl.NumberFormat('pt-BR', { style: 'currency', currency: item.currency || 'BRL' }).format(item.price)}
</p>
)}
{showQuantitySummary && (
<p className="text-sm text-gray-500 dark:text-gray-400 mb-3">
{item.claimedQuantity} de {item.quantity} reservados
@@ -242,7 +247,9 @@ function PublicWishlistContent() {
<li key={c.id} className="flex items-center justify-between gap-2">
<div>
<span className="font-medium">{isMine ? 'Você' : 'Reservado'}</span>
<span className="text-gray-500"> · {c.quantity} un.</span>
{item.quantity > 1 && (
<span className="text-gray-500"> · {c.quantity} un.</span>
)}
{c.note && isMine && (
<span className="block text-xs italic text-gray-500">
&quot;{c.note}&quot;

View File

@@ -14,7 +14,6 @@ import {
} from '@/lib/api';
import Header from '@/components/header';
import Link from 'next/link';
import SettingsSection from '@/components/admin/SettingsSection';
import ItemCard from '@/components/admin/ItemCard';
import ItemForm from '@/components/admin/ItemForm';
import ImageUpload from '@/components/image-upload';
@@ -42,7 +41,7 @@ function AdminPageContent() {
homepageSubtext: '',
});
// Wishlist edit state
// Unified config edit state
const [isEditingWishlist, setIsEditingWishlist] = useState(false);
const [editForm, setEditForm] = useState({
name: '',
@@ -51,6 +50,8 @@ function AdminPageContent() {
preferences: '',
imageUrl: '',
isPublic: true,
siteTitle: '',
homepageSubtext: '',
});
const [editError, setEditError] = useState('');
const [isWishlistImageUploading, setIsWishlistImageUploading] = useState(false);
@@ -105,6 +106,8 @@ function AdminPageContent() {
preferences: wishlist.preferences || '',
imageUrl: wishlist.imageUrl || '',
isPublic: wishlist.isPublic,
siteTitle: settings.siteTitle,
homepageSubtext: settings.homepageSubtext,
});
setEditError('');
setIsEditingWishlist(true);
@@ -115,11 +118,16 @@ function AdminPageContent() {
if (!wishlist) return;
setEditError('');
try {
const updated = await wishlistsApi.update(wishlist.id, editForm);
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 wishlist');
setEditError(error.message || 'Failed to update settings');
}
};
@@ -181,10 +189,6 @@ function AdminPageContent() {
}
};
const handleUpdateSettings = async (updatedSettings: Settings) => {
await settingsApi.updateSettings(updatedSettings);
setSettings(updatedSettings);
};
const editingItem = items.find((item) => item.id === editingItemId);
@@ -229,55 +233,72 @@ function AdminPageContent() {
</div>
) : (
<>
{/* Settings Section */}
<SettingsSection settings={settings} onUpdate={handleUpdateSettings} />
{/* Wishlist Info Section */}
{/* 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">
<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 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">
Nome *
Mensagem de boas-vindas
</label>
<input
type="text"
required
value={editForm.name}
onChange={(e) =>
setEditForm((prev) => ({ ...prev, name: e.target.value }))
}
<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
Descrição da lista
</label>
<textarea
value={editForm.description}
onChange={(e) =>
setEditForm((prev) => ({ ...prev, description: e.target.value }))
}
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}
/>
@@ -288,12 +309,17 @@ function AdminPageContent() {
</label>
<RichTextEditor
value={editForm.preferences}
onChange={(html) =>
setEditForm((prev) => ({ ...prev, preferences: html }))
}
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"
@@ -312,33 +338,38 @@ function AdminPageContent() {
</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>
<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"
/>
)}
<p className="text-sm text-gray-500 dark:text-gray-500 mt-1">
/{wishlist.slug}
</p>
<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>
<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>

View File

@@ -81,6 +81,8 @@ export async function PATCH(
const {
name,
description,
price,
currency,
quantity,
imageUrl,
purchaseUrls,
@@ -108,6 +110,8 @@ export async function PATCH(
if (name !== undefined) updateData.name = name;
if (description !== undefined) updateData.description = description;
if (price !== undefined) updateData.price = price != null ? Number(price) : null;
if (currency !== undefined) updateData.currency = currency;
if (quantity !== undefined) updateData.quantity = quantity;
if (imageUrl !== undefined) updateData.imageUrl = imageUrl;
if (purchaseUrls !== undefined) updateData.purchaseUrls = purchaseUrls;

View File

@@ -82,6 +82,8 @@ export async function POST(
const {
name,
description,
price,
currency,
quantity,
imageUrl,
purchaseUrls,
@@ -126,6 +128,8 @@ export async function POST(
wishlistId: id,
name,
description: description || null,
price: price != null ? Number(price) : null,
currency: currency || 'BRL',
quantity: quantity || 1,
imageUrl: imageUrl || null,
purchaseUrls: purchaseUrls || null,

View File

@@ -64,6 +64,17 @@ export default function ItemCard({
<h5 className="text-base font-semibold text-gray-900 dark:text-white">
{item.name}
</h5>
{item.price != null && (
<p className="text-sm text-indigo-600 dark:text-indigo-400 mt-0.5">
{new Intl.NumberFormat('pt-BR', { style: 'currency', currency: item.currency || 'BRL' }).format(item.price)}
{item.quantity > 1 && ` · Qtd: ${item.quantity}`}
</p>
)}
{item.price == null && item.quantity > 1 && (
<p className="text-sm text-gray-500 dark:text-gray-400 mt-0.5">
Qtd: {item.quantity}
</p>
)}
{item.description && (
<p className="text-base text-gray-600 dark:text-gray-400 mt-1">
{item.description}

View File

@@ -18,6 +18,8 @@ export default function ItemForm({ item, onSubmit, onCancel, mode, error }: Item
item || {
name: '',
description: '',
price: null,
currency: 'BRL',
quantity: 1,
imageUrl: '',
purchaseUrls: [],
@@ -80,6 +82,25 @@ export default function ItemForm({ item, onSubmit, onCancel, mode, error }: Item
}
/>
</div>
<div>
<label className="block text-base font-medium text-gray-700 dark:text-gray-300 mb-1">
Preço (R$)
</label>
<input
type="number"
min={0}
step="0.01"
placeholder="0,00"
className="w-full px-2 py-1.5 text-base border border-gray-300 dark:border-gray-600 rounded focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white"
value={formData.price ?? ''}
onChange={(e) =>
setFormData((prev) => ({
...prev,
price: e.target.value === '' ? null : parseFloat(e.target.value) || null,
}))
}
/>
</div>
<div className="md:col-span-2">
<label className="block text-base font-medium text-gray-700 dark:text-gray-300 mb-1">
Description

View File

@@ -147,6 +147,8 @@ export interface Item {
wishlistId: string;
name: string;
description: string | null;
price: number | null;
currency: string;
quantity: number;
imageUrl: string | null;
purchaseUrls: Array<{ label: string; url: string }> | null;