feat: grid layout, global claim/qty toggles, admin access link, swaddle image

- Public wishlist now renders as responsive 3-col grid instead of list
- Subtitle supports line breaks (whitespace-pre-line)
- claimingEnabled and showQuantity moved to global site settings (not per-item); toggled in admin Configurações panel; claim API enforces server-side
- Admin dashboard shows admin access link with copy button
- Settings API exposes and persists the two new boolean settings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Adriano Belisario
2026-05-03 22:52:32 +00:00
parent 5548f5000f
commit b19a3fdf48
8 changed files with 326 additions and 166 deletions

View File

@@ -39,7 +39,11 @@ function AdminPageContent() {
const [settings, setSettings] = useState<Settings>({
siteTitle: 'Wishlist',
homepageSubtext: '',
claimingEnabled: true,
showQuantity: true,
});
const [adminToken, setAdminToken] = useState<string | null>(null);
const [linkCopied, setLinkCopied] = useState(false);
// Unified config edit state
const [isEditingWishlist, setIsEditingWishlist] = useState(false);
@@ -52,6 +56,8 @@ function AdminPageContent() {
isPublic: true,
siteTitle: '',
homepageSubtext: '',
claimingEnabled: true,
showQuantity: true,
});
const [editError, setEditError] = useState('');
const [isWishlistImageUploading, setIsWishlistImageUploading] = useState(false);
@@ -68,8 +74,27 @@ function AdminPageContent() {
useEffect(() => {
fetchData();
fetchAdminToken();
}, []);
const fetchAdminToken = async () => {
try {
const res = await fetch('/api/admin/access-link', { credentials: 'include' });
if (res.ok) {
const data = await res.json();
setAdminToken(data.token);
}
} catch { /* ignore */ }
};
const copyAdminLink = async () => {
if (!adminToken) return;
const url = `${window.location.origin}/?adm=${adminToken}`;
await navigator.clipboard.writeText(url);
setLinkCopied(true);
setTimeout(() => setLinkCopied(false), 2000);
};
const fetchData = async () => {
try {
const [lists, settingsData] = await Promise.all([
@@ -108,6 +133,8 @@ function AdminPageContent() {
isPublic: wishlist.isPublic,
siteTitle: settings.siteTitle,
homepageSubtext: settings.homepageSubtext,
claimingEnabled: settings.claimingEnabled,
showQuantity: settings.showQuantity,
});
setEditError('');
setIsEditingWishlist(true);
@@ -118,13 +145,13 @@ function AdminPageContent() {
if (!wishlist) return;
setEditError('');
try {
const { siteTitle, homepageSubtext, ...wishlistFields } = editForm;
const { siteTitle, homepageSubtext, claimingEnabled, showQuantity, ...wishlistFields } = editForm;
const [updated] = await Promise.all([
wishlistsApi.update(wishlist.id, wishlistFields),
settingsApi.updateSettings({ siteTitle, homepageSubtext }),
settingsApi.updateSettings({ siteTitle, homepageSubtext, claimingEnabled, showQuantity }),
]);
setWishlist(updated);
setSettings({ siteTitle, homepageSubtext });
setSettings({ siteTitle, homepageSubtext, claimingEnabled, showQuantity });
setIsEditingWishlist(false);
} catch (error: any) {
setEditError(error.message || 'Failed to update settings');
@@ -292,6 +319,26 @@ function AdminPageContent() {
rows={2}
/>
</div>
<div className="flex gap-6">
<label className="flex items-center gap-2 cursor-pointer select-none">
<input
type="checkbox"
className="w-4 h-4 rounded accent-indigo-600"
checked={editForm.claimingEnabled}
onChange={(e) => setEditForm((prev) => ({ ...prev, claimingEnabled: e.target.checked }))}
/>
<span className="text-sm text-gray-700 dark:text-gray-300">Permitir reservas</span>
</label>
<label className="flex items-center gap-2 cursor-pointer select-none">
<input
type="checkbox"
className="w-4 h-4 rounded accent-indigo-600"
checked={editForm.showQuantity}
onChange={(e) => setEditForm((prev) => ({ ...prev, showQuantity: e.target.checked }))}
/>
<span className="text-sm text-gray-700 dark:text-gray-300">Mostrar quantidade</span>
</label>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Descrição da lista
@@ -370,6 +417,30 @@ function AdminPageContent() {
)}
</div>
</div>
<div className="flex gap-4 mt-1">
<span className={`text-xs px-2 py-0.5 rounded-full font-medium ${settings.claimingEnabled ? 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400' : 'bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-400'}`}>
Reservas {settings.claimingEnabled ? 'ativas' : 'desativadas'}
</span>
<span className={`text-xs px-2 py-0.5 rounded-full font-medium ${settings.showQuantity ? 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400' : 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-700 dark:text-yellow-400'}`}>
Quantidade {settings.showQuantity ? 'visível' : 'oculta'}
</span>
</div>
{adminToken && (
<div className="mt-4 pt-4 border-t border-gray-200 dark:border-gray-700">
<p className="text-xs font-medium text-gray-500 dark:text-gray-400 mb-2">Link de acesso admin</p>
<div className="flex items-center gap-2">
<code className="flex-1 text-xs bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 px-3 py-2 rounded truncate">
{`${typeof window !== 'undefined' ? window.location.origin : ''}/?adm=${adminToken}`}
</code>
<button
onClick={copyAdminLink}
className="flex-shrink-0 px-3 py-2 text-xs font-medium rounded border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors cursor-pointer"
>
{linkCopied ? 'Copiado!' : 'Copiar'}
</button>
</div>
</div>
)}
</div>
)}
</div>