diff --git a/app/admin/page.tsx b/app/admin/page.tsx new file mode 100644 index 0000000..53cdafa --- /dev/null +++ b/app/admin/page.tsx @@ -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([]); + 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 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} /> +
+ +
+ +