Compare commits
2 Commits
21e8b7e137
...
5548f5000f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5548f5000f | ||
|
|
3df9a67b97 |
@@ -221,6 +221,11 @@ function PublicWishlistContent() {
|
|||||||
<h3 className="text-2xl font-bold text-gray-900 dark:text-white mb-1">
|
<h3 className="text-2xl font-bold text-gray-900 dark:text-white mb-1">
|
||||||
{item.name}
|
{item.name}
|
||||||
</h3>
|
</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 && (
|
{showQuantitySummary && (
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400 mb-3">
|
<p className="text-sm text-gray-500 dark:text-gray-400 mb-3">
|
||||||
{item.claimedQuantity} de {item.quantity} reservados
|
{item.claimedQuantity} de {item.quantity} reservados
|
||||||
@@ -242,7 +247,9 @@ function PublicWishlistContent() {
|
|||||||
<li key={c.id} className="flex items-center justify-between gap-2">
|
<li key={c.id} className="flex items-center justify-between gap-2">
|
||||||
<div>
|
<div>
|
||||||
<span className="font-medium">{isMine ? 'Você' : 'Reservado'}</span>
|
<span className="font-medium">{isMine ? 'Você' : 'Reservado'}</span>
|
||||||
|
{item.quantity > 1 && (
|
||||||
<span className="text-gray-500"> · {c.quantity} un.</span>
|
<span className="text-gray-500"> · {c.quantity} un.</span>
|
||||||
|
)}
|
||||||
{c.note && isMine && (
|
{c.note && isMine && (
|
||||||
<span className="block text-xs italic text-gray-500">
|
<span className="block text-xs italic text-gray-500">
|
||||||
"{c.note}"
|
"{c.note}"
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import {
|
|||||||
} from '@/lib/api';
|
} from '@/lib/api';
|
||||||
import Header from '@/components/header';
|
import Header from '@/components/header';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import SettingsSection from '@/components/admin/SettingsSection';
|
|
||||||
import ItemCard from '@/components/admin/ItemCard';
|
import ItemCard from '@/components/admin/ItemCard';
|
||||||
import ItemForm from '@/components/admin/ItemForm';
|
import ItemForm from '@/components/admin/ItemForm';
|
||||||
import ImageUpload from '@/components/image-upload';
|
import ImageUpload from '@/components/image-upload';
|
||||||
@@ -42,7 +41,7 @@ function AdminPageContent() {
|
|||||||
homepageSubtext: '',
|
homepageSubtext: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Wishlist edit state
|
// Unified config edit state
|
||||||
const [isEditingWishlist, setIsEditingWishlist] = useState(false);
|
const [isEditingWishlist, setIsEditingWishlist] = useState(false);
|
||||||
const [editForm, setEditForm] = useState({
|
const [editForm, setEditForm] = useState({
|
||||||
name: '',
|
name: '',
|
||||||
@@ -51,6 +50,8 @@ function AdminPageContent() {
|
|||||||
preferences: '',
|
preferences: '',
|
||||||
imageUrl: '',
|
imageUrl: '',
|
||||||
isPublic: true,
|
isPublic: true,
|
||||||
|
siteTitle: '',
|
||||||
|
homepageSubtext: '',
|
||||||
});
|
});
|
||||||
const [editError, setEditError] = useState('');
|
const [editError, setEditError] = useState('');
|
||||||
const [isWishlistImageUploading, setIsWishlistImageUploading] = useState(false);
|
const [isWishlistImageUploading, setIsWishlistImageUploading] = useState(false);
|
||||||
@@ -105,6 +106,8 @@ function AdminPageContent() {
|
|||||||
preferences: wishlist.preferences || '',
|
preferences: wishlist.preferences || '',
|
||||||
imageUrl: wishlist.imageUrl || '',
|
imageUrl: wishlist.imageUrl || '',
|
||||||
isPublic: wishlist.isPublic,
|
isPublic: wishlist.isPublic,
|
||||||
|
siteTitle: settings.siteTitle,
|
||||||
|
homepageSubtext: settings.homepageSubtext,
|
||||||
});
|
});
|
||||||
setEditError('');
|
setEditError('');
|
||||||
setIsEditingWishlist(true);
|
setIsEditingWishlist(true);
|
||||||
@@ -115,11 +118,16 @@ function AdminPageContent() {
|
|||||||
if (!wishlist) return;
|
if (!wishlist) return;
|
||||||
setEditError('');
|
setEditError('');
|
||||||
try {
|
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);
|
setWishlist(updated);
|
||||||
|
setSettings({ siteTitle, homepageSubtext });
|
||||||
setIsEditingWishlist(false);
|
setIsEditingWishlist(false);
|
||||||
} catch (error: any) {
|
} 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);
|
const editingItem = items.find((item) => item.id === editingItemId);
|
||||||
|
|
||||||
@@ -229,55 +233,72 @@ function AdminPageContent() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{/* Settings Section */}
|
{/* Unified Settings & Wishlist Config */}
|
||||||
<SettingsSection settings={settings} onUpdate={handleUpdateSettings} />
|
|
||||||
|
|
||||||
{/* Wishlist Info Section */}
|
|
||||||
{wishlist && (
|
{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="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">
|
<div className="p-5">
|
||||||
{isEditingWishlist ? (
|
{isEditingWishlist ? (
|
||||||
<form onSubmit={handleUpdateWishlist} className="space-y-4">
|
<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 && (
|
{editError && (
|
||||||
<div className="p-3 bg-red-50 dark:bg-red-900/20 text-red-800 dark:text-red-400 rounded-lg text-base">
|
<div className="p-3 bg-red-50 dark:bg-red-900/20 text-red-800 dark:text-red-400 rounded-lg text-base">
|
||||||
{editError}
|
{editError}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<ImageUpload
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
currentImageUrl={editForm.imageUrl}
|
|
||||||
onImageChange={(url) =>
|
|
||||||
setEditForm((prev) => ({ ...prev, imageUrl: url }))
|
|
||||||
}
|
|
||||||
onUploadStateChange={setIsWishlistImageUploading}
|
|
||||||
type="wishlist"
|
|
||||||
label="Imagem da Lista"
|
|
||||||
/>
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
Nome *
|
Título do site *
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
required
|
required
|
||||||
value={editForm.name}
|
value={editForm.siteTitle}
|
||||||
onChange={(e) =>
|
onChange={(e) => setEditForm((prev) => ({ ...prev, siteTitle: e.target.value }))}
|
||||||
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"
|
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>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
Descrição
|
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>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
value={editForm.description}
|
value={editForm.description}
|
||||||
onChange={(e) =>
|
onChange={(e) => setEditForm((prev) => ({ ...prev, description: e.target.value }))}
|
||||||
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"
|
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}
|
rows={2}
|
||||||
/>
|
/>
|
||||||
@@ -288,12 +309,17 @@ function AdminPageContent() {
|
|||||||
</label>
|
</label>
|
||||||
<RichTextEditor
|
<RichTextEditor
|
||||||
value={editForm.preferences}
|
value={editForm.preferences}
|
||||||
onChange={(html) =>
|
onChange={(html) => setEditForm((prev) => ({ ...prev, preferences: html }))}
|
||||||
setEditForm((prev) => ({ ...prev, preferences: html }))
|
|
||||||
}
|
|
||||||
placeholder="Interesses e preferências gerais..."
|
placeholder="Interesses e preferências gerais..."
|
||||||
/>
|
/>
|
||||||
</div>
|
</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">
|
<div className="flex gap-3 justify-end">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -312,33 +338,38 @@ function AdminPageContent() {
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
) : (
|
) : (
|
||||||
|
<div className="space-y-3">
|
||||||
<div className="flex items-start gap-4">
|
<div className="flex items-start gap-4">
|
||||||
{wishlist.imageUrl && (
|
{wishlist.imageUrl && (
|
||||||
<img
|
<img
|
||||||
src={wishlist.imageUrl}
|
src={wishlist.imageUrl}
|
||||||
alt={wishlist.name}
|
alt={wishlist.name}
|
||||||
className="w-20 h-20 object-cover rounded border border-gray-200 dark:border-gray-600 flex-shrink-0"
|
className="w-16 h-16 object-cover rounded border border-gray-200 dark:border-gray-600 flex-shrink-0"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div className="flex-1">
|
<div className="flex-1 grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-2">
|
||||||
<h2 className="text-xl font-bold text-gray-900 dark:text-white">
|
<div>
|
||||||
{wishlist.name}
|
<p className="text-xs font-medium text-gray-500 dark:text-gray-400">Título do site</p>
|
||||||
</h2>
|
<p className="text-sm text-gray-900 dark:text-white">{settings.siteTitle}</p>
|
||||||
{wishlist.description && (
|
</div>
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
<div>
|
||||||
{wishlist.description}
|
<p className="text-xs font-medium text-gray-500 dark:text-gray-400">Nome da lista</p>
|
||||||
</p>
|
<p className="text-sm text-gray-900 dark:text-white">{wishlist.name}</p>
|
||||||
)}
|
</div>
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-500 mt-1">
|
{settings.homepageSubtext && (
|
||||||
/{wishlist.slug}
|
<div className="md:col-span-2">
|
||||||
</p>
|
<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>
|
||||||
<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>
|
</div>
|
||||||
|
|||||||
@@ -81,6 +81,8 @@ export async function PATCH(
|
|||||||
const {
|
const {
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
|
price,
|
||||||
|
currency,
|
||||||
quantity,
|
quantity,
|
||||||
imageUrl,
|
imageUrl,
|
||||||
purchaseUrls,
|
purchaseUrls,
|
||||||
@@ -108,6 +110,8 @@ export async function PATCH(
|
|||||||
|
|
||||||
if (name !== undefined) updateData.name = name;
|
if (name !== undefined) updateData.name = name;
|
||||||
if (description !== undefined) updateData.description = description;
|
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 (quantity !== undefined) updateData.quantity = quantity;
|
||||||
if (imageUrl !== undefined) updateData.imageUrl = imageUrl;
|
if (imageUrl !== undefined) updateData.imageUrl = imageUrl;
|
||||||
if (purchaseUrls !== undefined) updateData.purchaseUrls = purchaseUrls;
|
if (purchaseUrls !== undefined) updateData.purchaseUrls = purchaseUrls;
|
||||||
|
|||||||
@@ -82,6 +82,8 @@ export async function POST(
|
|||||||
const {
|
const {
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
|
price,
|
||||||
|
currency,
|
||||||
quantity,
|
quantity,
|
||||||
imageUrl,
|
imageUrl,
|
||||||
purchaseUrls,
|
purchaseUrls,
|
||||||
@@ -126,6 +128,8 @@ export async function POST(
|
|||||||
wishlistId: id,
|
wishlistId: id,
|
||||||
name,
|
name,
|
||||||
description: description || null,
|
description: description || null,
|
||||||
|
price: price != null ? Number(price) : null,
|
||||||
|
currency: currency || 'BRL',
|
||||||
quantity: quantity || 1,
|
quantity: quantity || 1,
|
||||||
imageUrl: imageUrl || null,
|
imageUrl: imageUrl || null,
|
||||||
purchaseUrls: purchaseUrls || null,
|
purchaseUrls: purchaseUrls || null,
|
||||||
|
|||||||
@@ -64,6 +64,17 @@ export default function ItemCard({
|
|||||||
<h5 className="text-base font-semibold text-gray-900 dark:text-white">
|
<h5 className="text-base font-semibold text-gray-900 dark:text-white">
|
||||||
{item.name}
|
{item.name}
|
||||||
</h5>
|
</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 && (
|
{item.description && (
|
||||||
<p className="text-base text-gray-600 dark:text-gray-400 mt-1">
|
<p className="text-base text-gray-600 dark:text-gray-400 mt-1">
|
||||||
{item.description}
|
{item.description}
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ export default function ItemForm({ item, onSubmit, onCancel, mode, error }: Item
|
|||||||
item || {
|
item || {
|
||||||
name: '',
|
name: '',
|
||||||
description: '',
|
description: '',
|
||||||
|
price: null,
|
||||||
|
currency: 'BRL',
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
imageUrl: '',
|
imageUrl: '',
|
||||||
purchaseUrls: [],
|
purchaseUrls: [],
|
||||||
@@ -80,6 +82,25 @@ export default function ItemForm({ item, onSubmit, onCancel, mode, error }: Item
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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">
|
<div className="md:col-span-2">
|
||||||
<label className="block text-base font-medium text-gray-700 dark:text-gray-300 mb-1">
|
<label className="block text-base font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
Description
|
Description
|
||||||
|
|||||||
@@ -147,6 +147,8 @@ export interface Item {
|
|||||||
wishlistId: string;
|
wishlistId: string;
|
||||||
name: string;
|
name: string;
|
||||||
description: string | null;
|
description: string | null;
|
||||||
|
price: number | null;
|
||||||
|
currency: string;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
imageUrl: string | null;
|
imageUrl: string | null;
|
||||||
purchaseUrls: Array<{ label: string; url: string }> | null;
|
purchaseUrls: Array<{ label: string; url: string }> | null;
|
||||||
|
|||||||
Reference in New Issue
Block a user