'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([]); const [isEditing, setIsEditing] = useState | null>(null); // Portal State const [portal, setPortal] = useState>({}); const [savingPortal, setSavingPortal] = useState(false); const [uploading, setUploading] = useState<{ [key: string]: boolean }>({}); // NEW UI STATES: Toast and Confirm Dialog const [toast, setToast] = useState(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, 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 handleUploadExtraImage = async (e: React.ChangeEvent) => { const files = e.target.files; if (!files || files.length === 0) return; setUploading(prev => ({ ...prev, extraImages: true })); const uploaded: string[] = []; for (const file of Array.from(files)) { const formData = new FormData(); formData.append('file', file); const res = await fetch('/api/upload', { method: 'POST', body: formData }); const data = await res.json(); if (data.url) uploaded.push(data.url); } setIsEditing(prev => ({ ...prev, extraImages: [...(prev?.extraImages || []), ...uploaded], })); setUploading(prev => ({ ...prev, extraImages: false })); // Reset input so the same file can be re-selected if needed e.target.value = ''; }; const removeExtraImage = (index: number) => { setIsEditing(prev => ({ ...prev, extraImages: (prev?.extraImages || []).filter((_, i) => i !== index), })); }; 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 (
{/* Top Header */}

Captive Portal CMS

Local Administration

View Live Portal ↗
{/* Tab Navigation */}
{/* TAB: CARDS */} {activeTab === 'cards' && (

Card Grid

{cards.length === 0 &&

No cards available. Create one to get started.

} {cards.map((card, idx) => ( // CHANGED: flex-col on mobile, flex-row on sm+, added gap-4 for mobile spacing
{card.imageUrl ? :
No Image
}
{card.title} {card.cardType}
{/* CHANGED: flex-wrap to ensure buttons don't overflow on small screens, w-full on mobile */}
))}
)} {/* TAB: SETTINGS */} {activeTab === 'settings' && (

Global Portal Settings

setPortal({...portal, title: e.target.value})} className={inputClasses} />