|
|
|
@@ -0,0 +1,388 @@ |
|
|
|
'use client';
|
|
|
|
|
|
|
|
import { useState, useEffect } from 'react';
|
|
|
|
import { Card, Portal } from '@/types';
|
|
|
|
|
|
|
|
export default function AdminDashboard() {
|
|
|
|
const [activeTab, setActiveTab] = useState<'cards' | 'settings'>('cards');
|
|
|
|
|
|
|
|
// Card State
|
|
|
|
const [cards, setCards] = useState<Card[]>([]);
|
|
|
|
const [isEditing, setIsEditing] = useState<Partial<Card> | null>(null);
|
|
|
|
|
|
|
|
// Portal State
|
|
|
|
const [portal, setPortal] = useState<Partial<Portal>>({});
|
|
|
|
const [savingPortal, setSavingPortal] = useState(false);
|
|
|
|
const [uploading, setUploading] = useState<{ [key: string]: boolean }>({});
|
|
|
|
|
|
|
|
// NEW UI STATES: Toast and Confirm Dialog
|
|
|
|
const [toast, setToast] = useState<string | null>(null);
|
|
|
|
const [confirmDialog, setConfirmDialog] = useState<{ message: string, onConfirm: () => void } | null>(null);
|
|
|
|
|
|
|
|
// Helper to show auto-dismissing toast
|
|
|
|
const showToast = (message: string) => {
|
|
|
|
setToast(message);
|
|
|
|
setTimeout(() => setToast(null), 3000);
|
|
|
|
};
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
fetch('/api/cards').then(res => res.json()).then(setCards);
|
|
|
|
fetch('/api/portals').then(res => res.json()).then(data => data && setPortal(data));
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
const handleUpload = async (e: React.ChangeEvent<HTMLInputElement>, field: string, isPortal = false) => {
|
|
|
|
if (!e.target.files?.[0]) return;
|
|
|
|
setUploading(prev => ({ ...prev, [field]: true }));
|
|
|
|
|
|
|
|
const formData = new FormData();
|
|
|
|
formData.append('file', e.target.files[0]);
|
|
|
|
const res = await fetch('/api/upload', { method: 'POST', body: formData });
|
|
|
|
const data = await res.json();
|
|
|
|
|
|
|
|
if (data.url) {
|
|
|
|
if (isPortal) {
|
|
|
|
setPortal(prev => ({ ...prev, [field]: data.url }));
|
|
|
|
} else {
|
|
|
|
setIsEditing(prev => ({ ...prev, [field]: data.url }));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
setUploading(prev => ({ ...prev, [field]: false }));
|
|
|
|
};
|
|
|
|
|
|
|
|
const handleSaveCard = async () => {
|
|
|
|
if (!isEditing) return;
|
|
|
|
const generateSafeId = () => 'card-' + Date.now().toString(36) + Math.random().toString(36).substring(2);
|
|
|
|
const newCard = { ...isEditing, id: isEditing.id || generateSafeId() } as Card;
|
|
|
|
|
|
|
|
await fetch('/api/cards', {
|
|
|
|
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(newCard)
|
|
|
|
});
|
|
|
|
|
|
|
|
setCards(prev => {
|
|
|
|
const exists = prev.find(c => c.id === newCard.id);
|
|
|
|
return exists ? prev.map(c => c.id === newCard.id ? newCard : c) : [...prev, newCard];
|
|
|
|
});
|
|
|
|
setIsEditing(null);
|
|
|
|
};
|
|
|
|
|
|
|
|
const handleDeleteCard = (id: string) => {
|
|
|
|
// Replace window.confirm with our custom dialog
|
|
|
|
setConfirmDialog({
|
|
|
|
message: 'Are you sure you want to delete this card? This action cannot be undone.',
|
|
|
|
onConfirm: async () => {
|
|
|
|
await fetch(`/api/cards?id=${id}`, { method: 'DELETE' });
|
|
|
|
setCards(prev => prev.filter(c => c.id !== id));
|
|
|
|
setConfirmDialog(null);
|
|
|
|
showToast('Card successfully deleted.');
|
|
|
|
}
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
const moveCard = async (index: number, direction: 'up' | 'down') => {
|
|
|
|
const newCards = [...cards];
|
|
|
|
|
|
|
|
if (direction === 'up' && index > 0) {
|
|
|
|
[newCards[index - 1], newCards[index]] = [newCards[index], newCards[index - 1]];
|
|
|
|
} else if (direction === 'down' && index < newCards.length - 1) {
|
|
|
|
[newCards[index + 1], newCards[index]] = [newCards[index], newCards[index + 1]];
|
|
|
|
} else {
|
|
|
|
return; // Do nothing if trying to move out of bounds
|
|
|
|
}
|
|
|
|
|
|
|
|
// Recalculate displayOrder for the whole array
|
|
|
|
const updatedCards = newCards.map((c, i) => ({ ...c, displayOrder: i }));
|
|
|
|
|
|
|
|
// Optimistically update the UI
|
|
|
|
setCards(updatedCards);
|
|
|
|
|
|
|
|
// Persist the new order to the backend
|
|
|
|
await fetch('/api/cards', {
|
|
|
|
method: 'PUT',
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
body: JSON.stringify(updatedCards)
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
const handleSavePortal = async () => {
|
|
|
|
setSavingPortal(true);
|
|
|
|
await fetch('/api/portals', {
|
|
|
|
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(portal)
|
|
|
|
});
|
|
|
|
setSavingPortal(false);
|
|
|
|
showToast('Portal settings saved successfully!'); // Replaced window.alert
|
|
|
|
};
|
|
|
|
|
|
|
|
// Shared Input Classes for high contrast
|
|
|
|
const inputClasses = "w-full border border-gray-300 p-2.5 rounded-lg outline-none focus:ring-2 focus:ring-blue-500 bg-white text-gray-900 placeholder-gray-400";
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div className="min-h-screen bg-gray-50 font-sans pb-12">
|
|
|
|
{/* Top Header */}
|
|
|
|
<div className="bg-blue-900 text-white shadow-md py-6 px-4">
|
|
|
|
<div className="max-w-5xl mx-auto flex justify-between items-center">
|
|
|
|
<div>
|
|
|
|
<h1 className="text-2xl font-bold">Captive Portal CMS</h1>
|
|
|
|
<p className="text-sm text-blue-200">Local Administration</p>
|
|
|
|
</div>
|
|
|
|
<a href="/" target="_blank" className="bg-blue-800 hover:bg-blue-700 text-white px-4 py-2 rounded text-sm transition-colors">
|
|
|
|
View Live Portal ↗
|
|
|
|
</a>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div className="max-w-5xl mx-auto mt-8 px-4">
|
|
|
|
{/* Tab Navigation */}
|
|
|
|
<div className="flex space-x-2 mb-6">
|
|
|
|
<button onClick={() => setActiveTab('cards')} className={`px-6 py-3 rounded-t-lg font-bold transition-colors ${activeTab === 'cards' ? 'bg-white text-blue-700 border-t-4 border-blue-600 shadow-sm' : 'bg-gray-200 text-gray-600 hover:bg-gray-300'}`}>
|
|
|
|
Manage Cards
|
|
|
|
</button>
|
|
|
|
<button onClick={() => setActiveTab('settings')} className={`px-6 py-3 rounded-t-lg font-bold transition-colors ${activeTab === 'settings' ? 'bg-white text-blue-700 border-t-4 border-blue-600 shadow-sm' : 'bg-gray-200 text-gray-600 hover:bg-gray-300'}`}>
|
|
|
|
Portal Settings
|
|
|
|
</button>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div className="bg-white rounded-b-xl rounded-tr-xl shadow-sm border border-gray-200 overflow-hidden min-h-[500px]">
|
|
|
|
|
|
|
|
{/* TAB: CARDS */}
|
|
|
|
{activeTab === 'cards' && (
|
|
|
|
<div className="p-6 md:p-8">
|
|
|
|
<div className="flex justify-between items-center mb-8 border-b pb-4">
|
|
|
|
<h2 className="text-xl font-bold text-gray-800">Card Grid</h2>
|
|
|
|
<button onClick={() => setIsEditing({ title: '', cardType: 'INFO_PAGE', displayOrder: cards.length })} className="bg-blue-600 text-white px-5 py-2.5 rounded-lg shadow-sm hover:bg-blue-700 font-medium">
|
|
|
|
+ Add New Card
|
|
|
|
</button>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div className="space-y-3 mb-8">
|
|
|
|
{cards.length === 0 && <p className="text-gray-500 italic text-center py-8">No cards available. Create one to get started.</p>}
|
|
|
|
{cards.map((card, idx) => (
|
|
|
|
// CHANGED: flex-col on mobile, flex-row on sm+, added gap-4 for mobile spacing
|
|
|
|
<div key={card.id} className="flex flex-col sm:flex-row sm:items-center justify-between p-4 border rounded-lg bg-gray-50 hover:bg-gray-100 transition-colors gap-4">
|
|
|
|
<div className="flex items-center gap-4">
|
|
|
|
{card.imageUrl ? <img src={card.imageUrl} className="w-16 h-16 object-cover rounded-md shadow-sm shrink-0" alt="" /> : <div className="w-16 h-16 bg-gray-200 rounded-md shadow-sm flex items-center justify-center text-gray-400 text-xs shrink-0">No Image</div>}
|
|
|
|
<div>
|
|
|
|
<span className="font-semibold text-gray-800 block">{card.title}</span>
|
|
|
|
<span className="text-xs text-gray-500 uppercase tracking-wider">{card.cardType}</span>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
{/* CHANGED: flex-wrap to ensure buttons don't overflow on small screens, w-full on mobile */}
|
|
|
|
<div className="flex flex-wrap items-center gap-2 w-full sm:w-auto justify-end">
|
|
|
|
<button onClick={() => moveCard(idx, 'up')} className="p-2 text-gray-500 hover:text-gray-800 hover:bg-gray-200 rounded" title="Move Up">↑</button>
|
|
|
|
<button onClick={() => moveCard(idx, 'down')} className="p-2 text-gray-500 hover:text-gray-800 hover:bg-gray-200 rounded" title="Move Down">↓</button>
|
|
|
|
<div className="w-px h-6 bg-gray-300 mx-1 hidden sm:block"></div>
|
|
|
|
<button onClick={() => setIsEditing(card)} className="px-4 py-2 text-blue-600 hover:bg-blue-50 rounded font-medium">Edit</button>
|
|
|
|
<button onClick={() => handleDeleteCard(card.id)} className="px-4 py-2 text-red-600 hover:bg-red-50 rounded font-medium">Delete</button>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
))}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
|
|
|
|
{/* TAB: SETTINGS */}
|
|
|
|
{activeTab === 'settings' && (
|
|
|
|
<div className="p-6 md:p-8">
|
|
|
|
<h2 className="text-xl font-bold text-gray-800 mb-8 border-b pb-4">Global Portal Settings</h2>
|
|
|
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-10">
|
|
|
|
<div className="space-y-6">
|
|
|
|
<div>
|
|
|
|
<label className="block text-sm font-semibold text-gray-700 mb-1">Portal Title</label>
|
|
|
|
<input type="text" value={portal.title || ''} onChange={e => setPortal({...portal, title: e.target.value})} className={inputClasses} />
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div>
|
|
|
|
<label className="block text-sm font-semibold text-gray-700 mb-1">Welcome Text</label>
|
|
|
|
<textarea value={portal.welcomeText || ''} onChange={e => setPortal({...portal, welcomeText: e.target.value})} className={`${inputClasses} h-32 resize-none`} />
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div className="flex gap-8">
|
|
|
|
<div>
|
|
|
|
<label className="block text-sm font-semibold text-gray-700 mb-1">Theme Color</label>
|
|
|
|
<div className="flex items-center gap-4">
|
|
|
|
<input type="color" value={portal.themeColor || '#1e3a8a'} onChange={e => setPortal({...portal, themeColor: e.target.value})} className="h-12 w-12 rounded cursor-pointer border-0 p-0" />
|
|
|
|
<span className="text-gray-900 font-mono font-medium">{portal.themeColor || '#1e3a8a'}</span>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
{/* NEW: Max Columns Setting updated for 3 */}
|
|
|
|
<div className="flex-1">
|
|
|
|
<label className="block text-sm font-semibold text-gray-700 mb-1">Grid Max Columns: {portal.maxGridColumns || 5}</label>
|
|
|
|
<input
|
|
|
|
type="range"
|
|
|
|
min="3"
|
|
|
|
max="8"
|
|
|
|
value={portal.maxGridColumns || 5}
|
|
|
|
onChange={e => setPortal({...portal, maxGridColumns: parseInt(e.target.value)})}
|
|
|
|
className="w-full mt-3 accent-blue-600"
|
|
|
|
/>
|
|
|
|
<div className="flex justify-between text-xs text-gray-400 mt-1">
|
|
|
|
<span>3</span><span>4</span><span>5</span><span>6</span><span>7</span><span>8</span>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div className="space-y-6">
|
|
|
|
{/* Logo Upload with Remove Button */}
|
|
|
|
<div>
|
|
|
|
<label className="block text-sm font-semibold text-gray-700 mb-1">Logo Image</label>
|
|
|
|
<input type="file" accept="image/*" onChange={e => handleUpload(e, 'logoUrl', true)} className="block w-full text-sm text-gray-900 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:bg-gray-100 cursor-pointer" />
|
|
|
|
{uploading['logoUrl'] && <span className="text-xs text-blue-500">Uploading...</span>}
|
|
|
|
{portal.logoUrl && (
|
|
|
|
<div className="mt-2 bg-gray-100 p-4 rounded inline-block relative border">
|
|
|
|
<img src={portal.logoUrl} className="h-16 object-contain" alt="Logo Preview" />
|
|
|
|
<button onClick={() => setPortal({...portal, logoUrl: ''})} className="absolute -top-2 -right-2 bg-red-500 text-white w-6 h-6 rounded-full text-xs font-bold hover:bg-red-600 shadow">✕</button>
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
|
|
|
|
{/* Hero Upload with Remove Button */}
|
|
|
|
<div>
|
|
|
|
<label className="block text-sm font-semibold text-gray-700 mb-1">Hero Background Image</label>
|
|
|
|
<input type="file" accept="image/*" onChange={e => handleUpload(e, 'heroImageUrl', true)} className="block w-full text-sm text-gray-900 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:bg-gray-100 cursor-pointer" />
|
|
|
|
{uploading['heroImageUrl'] && <span className="text-xs text-blue-500">Uploading...</span>}
|
|
|
|
{portal.heroImageUrl && (
|
|
|
|
<div className="mt-2 relative rounded shadow border inline-block w-full">
|
|
|
|
<img src={portal.heroImageUrl} className="h-32 w-full object-cover rounded" alt="Hero Preview" />
|
|
|
|
<button onClick={() => setPortal({...portal, heroImageUrl: ''})} className="absolute top-2 right-2 bg-red-500 text-white w-8 h-8 flex items-center justify-center rounded-full text-sm font-bold hover:bg-red-600 shadow-lg">✕</button>
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div className="bg-gray-50 p-4 rounded-lg border border-gray-200">
|
|
|
|
<label className="flex items-center gap-3 cursor-pointer">
|
|
|
|
<input type="checkbox" checked={!!portal.fadeHeroImage} onChange={e => setPortal({...portal, fadeHeroImage: e.target.checked})} className="w-5 h-5 text-blue-600 rounded" />
|
|
|
|
<div>
|
|
|
|
<span className="block text-sm font-semibold text-gray-900">Fade Image into Background Color</span>
|
|
|
|
<span className="block text-xs text-gray-600">Creates a smooth gradient from the top of the image into the solid theme color at the bottom.</span>
|
|
|
|
</div>
|
|
|
|
</label>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div className="mt-10 pt-6 border-t border-gray-200 flex justify-end">
|
|
|
|
<button onClick={handleSavePortal} disabled={savingPortal} className="bg-blue-600 text-white px-10 py-3 rounded-lg hover:bg-blue-700 font-bold shadow disabled:opacity-50 transition-colors">
|
|
|
|
{savingPortal ? 'Saving...' : 'Save Portal Settings'}
|
|
|
|
</button>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
{/* MODAL FOR EDITING/CREATING CARDS */}
|
|
|
|
{isEditing && (
|
|
|
|
<div
|
|
|
|
className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm p-4 transition-opacity"
|
|
|
|
onClick={() => setIsEditing(null)} // Click outside to close
|
|
|
|
>
|
|
|
|
<div
|
|
|
|
className="bg-white rounded-2xl w-full max-w-3xl max-h-[90vh] overflow-y-auto shadow-2xl p-6 md:p-8 relative animate-in fade-in zoom-in-95 duration-200"
|
|
|
|
onClick={(e) => e.stopPropagation()} // Prevent inside clicks from closing
|
|
|
|
>
|
|
|
|
<button
|
|
|
|
onClick={() => setIsEditing(null)}
|
|
|
|
className="absolute top-6 right-6 text-gray-400 hover:text-gray-900 bg-gray-100 hover:bg-gray-200 rounded-full w-8 h-8 flex items-center justify-center transition-colors"
|
|
|
|
>
|
|
|
|
✕
|
|
|
|
</button>
|
|
|
|
|
|
|
|
<h3 className="text-2xl font-bold mb-6 text-gray-900 border-b pb-4">
|
|
|
|
{isEditing.id ? 'Edit Card' : 'Create New Card'}
|
|
|
|
</h3>
|
|
|
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
|
|
|
<div className="space-y-5">
|
|
|
|
<div>
|
|
|
|
<label className="block text-sm font-semibold text-gray-800 mb-1">Title</label>
|
|
|
|
<input type="text" value={isEditing.title || ''} onChange={e => setIsEditing({...isEditing, title: e.target.value})} className={inputClasses} placeholder="e.g., Local History" />
|
|
|
|
</div>
|
|
|
|
<div>
|
|
|
|
<label className="block text-sm font-semibold text-gray-800 mb-1">Card Type</label>
|
|
|
|
<select value={isEditing.cardType || 'INFO_PAGE'} onChange={e => setIsEditing({...isEditing, cardType: e.target.value as any})} className={inputClasses}>
|
|
|
|
<option value="INFO_PAGE">Info Page</option>
|
|
|
|
<option value="IMAGE_GALLERY">Image Gallery</option>
|
|
|
|
<option value="EXTERNAL_LINK">External Link</option>
|
|
|
|
</select>
|
|
|
|
</div>
|
|
|
|
<div>
|
|
|
|
<label className="block text-sm font-semibold text-gray-800 mb-1">Short Description</label>
|
|
|
|
<textarea value={isEditing.shortDescription || ''} onChange={e => setIsEditing({...isEditing, shortDescription: e.target.value})} className={`${inputClasses} h-24 resize-none`} placeholder="Brief summary..." />
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div className="space-y-5">
|
|
|
|
<div>
|
|
|
|
<label className="block text-sm font-semibold text-gray-800 mb-1">Cover Image</label>
|
|
|
|
<div className="border-2 border-dashed border-gray-300 rounded-lg p-4 text-center hover:bg-gray-50 transition-colors">
|
|
|
|
<input type="file" accept="image/*" onChange={e => handleUpload(e, 'imageUrl')} className="block w-full text-sm text-gray-700 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100 cursor-pointer" />
|
|
|
|
{uploading['imageUrl'] && <p className="mt-2 text-sm text-blue-600 font-medium">Uploading image...</p>}
|
|
|
|
</div>
|
|
|
|
{isEditing.imageUrl && (
|
|
|
|
<div className="mt-4 relative rounded-lg overflow-hidden border border-gray-200 group">
|
|
|
|
<img src={isEditing.imageUrl} className="w-full h-40 object-cover" alt="Preview" />
|
|
|
|
<button
|
|
|
|
onClick={() => setIsEditing({...isEditing, imageUrl: ''})}
|
|
|
|
className="absolute top-2 right-2 bg-red-500 text-white w-8 h-8 rounded-full text-sm font-bold shadow opacity-0 group-hover:opacity-100 transition-opacity hover:bg-red-600"
|
|
|
|
title="Remove Image"
|
|
|
|
>
|
|
|
|
✕
|
|
|
|
</button>
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div className="flex gap-3 pt-8 mt-6 border-t border-gray-200 justify-end">
|
|
|
|
<button onClick={() => setIsEditing(null)} className="px-5 py-2.5 text-gray-700 hover:bg-gray-100 rounded-lg font-medium transition-colors">
|
|
|
|
Cancel
|
|
|
|
</button>
|
|
|
|
<button onClick={handleSaveCard} className="bg-green-600 text-white px-8 py-2.5 rounded-lg hover:bg-green-700 font-medium shadow-sm transition-colors">
|
|
|
|
Save Card
|
|
|
|
</button>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
|
|
|
|
{/* CUSTOM CONFIRM DIALOG */}
|
|
|
|
{confirmDialog && (
|
|
|
|
<div className="fixed inset-0 z-[60] flex items-center justify-center bg-black/60 backdrop-blur-sm p-4 animate-in fade-in duration-200">
|
|
|
|
<div className="bg-white rounded-xl shadow-2xl p-6 max-w-sm w-full animate-in zoom-in-95">
|
|
|
|
<h3 className="text-xl font-bold text-gray-900 mb-2">Confirm Action</h3>
|
|
|
|
<p className="text-gray-600 mb-6 leading-relaxed">{confirmDialog.message}</p>
|
|
|
|
<div className="flex justify-end gap-3">
|
|
|
|
<button
|
|
|
|
onClick={() => setConfirmDialog(null)}
|
|
|
|
className="px-4 py-2.5 text-gray-700 hover:bg-gray-100 rounded-lg font-medium transition-colors"
|
|
|
|
>
|
|
|
|
Cancel
|
|
|
|
</button>
|
|
|
|
<button
|
|
|
|
onClick={confirmDialog.onConfirm}
|
|
|
|
className="px-6 py-2.5 bg-red-600 text-white rounded-lg hover:bg-red-700 font-medium transition-colors shadow-sm"
|
|
|
|
>
|
|
|
|
Delete
|
|
|
|
</button>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
|
|
|
|
{/* CUSTOM TOAST NOTIFICATION */}
|
|
|
|
{toast && (
|
|
|
|
<div className="fixed bottom-6 right-6 z-[70] bg-gray-900 text-white px-6 py-4 rounded-lg shadow-2xl flex items-center gap-3 animate-in slide-in-from-bottom-5 fade-in duration-300">
|
|
|
|
<div className="w-6 h-6 bg-green-500 rounded-full flex items-center justify-center text-gray-900 font-bold text-sm">
|
|
|
|
✓
|
|
|
|
</div>
|
|
|
|
<span className="font-medium">{toast}</span>
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
} |