685 lines
28 KiB
TypeScript
685 lines
28 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
import {
|
|
Bell,
|
|
Trash2,
|
|
Plus,
|
|
Layers,
|
|
Settings,
|
|
Upload,
|
|
Calendar,
|
|
X,
|
|
CheckCircle2,
|
|
AlertTriangle,
|
|
Globe,
|
|
Loader2,
|
|
FileImage
|
|
} from 'lucide-react';
|
|
import { cn } from '../../lib/utils';
|
|
import { API_BASE_URL } from '../../lib/config';
|
|
import { useDialog } from '../../context/DialogContext';
|
|
|
|
interface Notice {
|
|
id: string;
|
|
title: string;
|
|
message: string;
|
|
timestamp: string;
|
|
type: 'INFO' | 'URGENT' | 'DELAY';
|
|
image?: string;
|
|
expiresAt?: string;
|
|
}
|
|
|
|
interface SpecialEvent {
|
|
id: string;
|
|
title: string;
|
|
description: string;
|
|
link: string;
|
|
created_at: string;
|
|
}
|
|
|
|
export const ManageNoticesView: React.FC = () => {
|
|
const { showAlert, showConfirm } = useDialog();
|
|
|
|
// Navigation Tabs
|
|
const [activeTab, setActiveTab] = useState<'notices' | 'special-events'>('notices');
|
|
|
|
// Data States
|
|
const [notices, setNotices] = useState<Notice[]>([]);
|
|
const [specialEvents, setSpecialEvents] = useState<SpecialEvent[]>([]);
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
// Modal States
|
|
const [isNoticeModalOpen, setIsNoticeModalOpen] = useState(false);
|
|
const [isEventModalOpen, setIsEventModalOpen] = useState(false);
|
|
|
|
// Form States (Notice)
|
|
const [noticeTitle, setNoticeTitle] = useState('');
|
|
const [noticeMessage, setNoticeMessage] = useState('');
|
|
const [noticeType, setNoticeType] = useState<'INFO' | 'URGENT' | 'DELAY'>('INFO');
|
|
const [noticeImage, setNoticeImage] = useState<string | null>(null);
|
|
|
|
// Form States (Special Event)
|
|
const [eventTitle, setEventTitle] = useState('');
|
|
const [eventDesc, setEventDesc] = useState('');
|
|
const [eventLink, setEventLink] = useState('');
|
|
const [eventImage, setEventImage] = useState<string | null>(null);
|
|
|
|
const handleEventImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const file = e.target.files?.[0];
|
|
if (file) {
|
|
if (file.size > 1.5 * 1024 * 1024) {
|
|
showAlert('File Too Large', 'Please upload an image smaller than 1.5MB.', 'error');
|
|
return;
|
|
}
|
|
const reader = new FileReader();
|
|
reader.onloadend = () => {
|
|
setEventImage(reader.result as string);
|
|
};
|
|
reader.readAsDataURL(file);
|
|
}
|
|
};
|
|
|
|
|
|
useEffect(() => {
|
|
fetchInitialData();
|
|
}, []);
|
|
|
|
const fetchInitialData = async () => {
|
|
try {
|
|
setLoading(true);
|
|
const [noticesRes, eventsRes] = await Promise.all([
|
|
fetch(`${API_BASE_URL}/api/announcements`),
|
|
fetch(`${API_BASE_URL}/api/special-events`)
|
|
]);
|
|
|
|
if (noticesRes.ok) {
|
|
const noticesData = await noticesRes.json();
|
|
setNotices(noticesData);
|
|
}
|
|
if (eventsRes.ok) {
|
|
const eventsData = await eventsRes.json();
|
|
setSpecialEvents(eventsData);
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to load notice configuration data:', err);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
// Sticky notice limits check
|
|
const handleOpenNoticeModal = () => {
|
|
if (notices.length >= 3) {
|
|
showAlert(
|
|
'Limit Reached',
|
|
'A maximum of 3 notices can be active on the campus notice board at any time. Please delete an existing notice first.',
|
|
'info'
|
|
);
|
|
return;
|
|
}
|
|
setNoticeTitle('');
|
|
setNoticeMessage('');
|
|
setNoticeType('INFO');
|
|
setNoticeImage(null);
|
|
setIsNoticeModalOpen(true);
|
|
};
|
|
|
|
const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const file = e.target.files?.[0];
|
|
if (file) {
|
|
if (file.size > 1.5 * 1024 * 1024) {
|
|
showAlert('File Too Large', 'Please upload an image smaller than 1.5MB.', 'error');
|
|
return;
|
|
}
|
|
const reader = new FileReader();
|
|
reader.onloadend = () => {
|
|
setNoticeImage(reader.result as string);
|
|
};
|
|
reader.readAsDataURL(file);
|
|
}
|
|
};
|
|
|
|
const handleAddNotice = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (!noticeTitle.trim() || !noticeMessage.trim()) {
|
|
showAlert('Required Fields', 'Please fill in both title and message.', 'error');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE_URL}/api/announcements`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
title: noticeTitle.trim(),
|
|
message: noticeMessage.trim(),
|
|
type: noticeType,
|
|
image: noticeImage
|
|
})
|
|
});
|
|
|
|
if (response.ok) {
|
|
setIsNoticeModalOpen(false);
|
|
fetchInitialData();
|
|
showAlert('Success', 'Notice posted successfully to Campus Notice Board.', 'success');
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to post notice:', err);
|
|
showAlert('Error', 'Failed to save notice. Please try again.', 'error');
|
|
}
|
|
};
|
|
|
|
const handleDeleteNotice = async (id: string) => {
|
|
showConfirm(
|
|
'Remove Notice',
|
|
'Are you sure you want to remove this notice from the board?',
|
|
async () => {
|
|
try {
|
|
const response = await fetch(`${API_BASE_URL}/api/announcements/${id}`, {
|
|
method: 'DELETE'
|
|
});
|
|
if (response.ok) {
|
|
fetchInitialData();
|
|
showAlert('Success', 'Notice removed successfully.', 'success');
|
|
}
|
|
} catch (err) {
|
|
console.error('Delete failed:', err);
|
|
showAlert('Error', 'Failed to delete notice.', 'error');
|
|
}
|
|
}
|
|
);
|
|
};
|
|
|
|
|
|
|
|
// Special Events Updates
|
|
const handleAddSpecialEvent = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (!eventTitle.trim() || !eventDesc.trim() || !eventLink.trim()) {
|
|
showAlert('Required Fields', 'Please fill in all special event details.', 'error');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE_URL}/api/special-events`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
title: eventTitle.trim(),
|
|
description: eventDesc.trim(),
|
|
link: eventLink.trim(),
|
|
created_by: 'ADMIN',
|
|
image: eventImage
|
|
})
|
|
});
|
|
|
|
if (response.ok) {
|
|
setIsEventModalOpen(false);
|
|
setEventTitle('');
|
|
setEventDesc('');
|
|
setEventLink('');
|
|
setEventImage(null);
|
|
fetchInitialData();
|
|
showAlert('Success', 'Special event added to the registry successfully.', 'success');
|
|
}
|
|
} catch (err) {
|
|
console.error('Post failed:', err);
|
|
showAlert('Error', 'Failed to create special event.', 'error');
|
|
}
|
|
};
|
|
|
|
const handleDeleteSpecialEvent = async (id: string) => {
|
|
showConfirm(
|
|
'Delete Special Event',
|
|
'Are you sure you want to remove this event from the registry?',
|
|
async () => {
|
|
try {
|
|
const response = await fetch(`${API_BASE_URL}/api/special-events/${id}`, {
|
|
method: 'DELETE'
|
|
});
|
|
if (response.ok) {
|
|
fetchInitialData();
|
|
showAlert('Success', 'Special event deleted successfully.', 'success');
|
|
}
|
|
} catch (err) {
|
|
console.error('Delete failed:', err);
|
|
showAlert('Error', 'Failed to delete event.', 'error');
|
|
}
|
|
}
|
|
);
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="flex items-center justify-center p-24">
|
|
<Loader2 className="w-8 h-8 text-brand-indigo animate-spin" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-10 font-sans">
|
|
{/* Title */}
|
|
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6">
|
|
<div>
|
|
<h1 className="text-4xl font-black text-text-dark tracking-tight mb-1">Notices & Banner Settings</h1>
|
|
<p className="text-text-muted font-black uppercase tracking-widest text-[10px]">Configure notice boards, scroll announcements, & special events</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Tabs */}
|
|
<div className="flex border-b border-slate-100 gap-6">
|
|
<button
|
|
onClick={() => setActiveTab('notices')}
|
|
className={cn(
|
|
"pb-4 text-xs font-black uppercase tracking-widest border-b-2 transition-all flex items-center gap-2",
|
|
activeTab === 'notices'
|
|
? "border-brand-indigo text-brand-indigo"
|
|
: "border-transparent text-text-muted hover:text-brand-indigo/70"
|
|
)}
|
|
>
|
|
<Bell className="w-4 h-4" />
|
|
Notice Board ({notices.length}/3)
|
|
</button>
|
|
<button
|
|
onClick={() => setActiveTab('special-events')}
|
|
className={cn(
|
|
"pb-4 text-xs font-black uppercase tracking-widest border-b-2 transition-all flex items-center gap-2",
|
|
activeTab === 'special-events'
|
|
? "border-brand-indigo text-brand-indigo"
|
|
: "border-transparent text-text-muted hover:text-brand-indigo/70"
|
|
)}
|
|
>
|
|
<Layers className="w-4 h-4" />
|
|
Special Events
|
|
</button>
|
|
</div>
|
|
|
|
{/* Notices Tab */}
|
|
{activeTab === 'notices' && (
|
|
<div className="space-y-6">
|
|
<div className="flex justify-between items-center bg-slate-50 p-6 rounded-3xl border border-slate-100">
|
|
<div>
|
|
<h2 className="text-lg font-black text-text-dark">Active Campus Notices</h2>
|
|
<p className="text-xs text-text-muted font-bold">These cards are pinned to the student Campus Notice Board (Maximum 3 notices).</p>
|
|
</div>
|
|
<button
|
|
onClick={handleOpenNoticeModal}
|
|
className="flex items-center gap-2 bg-brand-indigo text-white px-5 py-3 rounded-2xl font-black text-xs uppercase tracking-widest hover:scale-[1.02] transition-all shadow-md shadow-brand-indigo/15"
|
|
>
|
|
<Plus className="w-4 h-4" />
|
|
Post Notice
|
|
</button>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
{notices.map(notice => (
|
|
<div key={notice.id} className="bg-white border border-slate-100 p-6 rounded-[2rem] premium-shadow flex flex-col justify-between min-h-[250px] relative">
|
|
<div>
|
|
<div className="flex items-center justify-between mb-4">
|
|
<span className={cn(
|
|
"px-2.5 py-1 text-[8px] font-black uppercase tracking-wider rounded",
|
|
notice.type === 'URGENT' ? 'bg-red-550/10 text-rose-600' :
|
|
notice.type === 'DELAY' ? 'bg-amber-500/10 text-amber-600' :
|
|
'bg-slate-100 text-slate-600'
|
|
)}>
|
|
{notice.type}
|
|
</span>
|
|
<button
|
|
onClick={() => handleDeleteNotice(notice.id)}
|
|
className="p-2 hover:bg-rose-50 text-slate-400 hover:text-rose-500 rounded-xl transition-all"
|
|
>
|
|
<Trash2 className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
|
|
{notice.image && (
|
|
<img
|
|
src={notice.image}
|
|
alt="Notice Attachment"
|
|
className="w-full h-36 object-cover rounded-2xl mb-4 border border-slate-100"
|
|
/>
|
|
)}
|
|
|
|
<h3 className="text-base font-black text-text-dark tracking-tight mb-2 leading-tight">{notice.title}</h3>
|
|
<p className="text-xs text-text-muted font-medium leading-relaxed mb-4">{notice.message}</p>
|
|
</div>
|
|
|
|
<div className="text-[10px] font-bold text-slate-400 border-t border-slate-50 pt-3 mt-auto">
|
|
{new Date(notice.timestamp).toLocaleString([], { dateStyle: 'medium', timeStyle: 'short' })}
|
|
</div>
|
|
</div>
|
|
))}
|
|
|
|
{notices.length === 0 && (
|
|
<div className="col-span-full py-16 text-center text-slate-400 font-bold uppercase tracking-wider text-xs border-2 border-dashed border-slate-200 rounded-[2.5rem]">
|
|
No active announcements on the notice board.
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
|
|
|
|
{/* Special Events Tab */}
|
|
{activeTab === 'special-events' && (
|
|
<div className="space-y-6">
|
|
<div className="flex justify-between items-center bg-slate-50 p-6 rounded-3xl border border-slate-100">
|
|
<div>
|
|
<h2 className="text-lg font-black text-text-dark">Special Events Registry</h2>
|
|
<p className="text-xs text-text-muted font-bold">These events are rendered in the modern slanted registry section of the student dashboard.</p>
|
|
</div>
|
|
<button
|
|
onClick={() => setIsEventModalOpen(true)}
|
|
className="flex items-center gap-2 bg-brand-indigo text-white px-5 py-3 rounded-2xl font-black text-xs uppercase tracking-widest hover:scale-[1.02] transition-all shadow-md shadow-brand-indigo/15"
|
|
>
|
|
<Plus className="w-4 h-4" />
|
|
Add Special Event
|
|
</button>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
{specialEvents.map(event => (
|
|
<div key={event.id} className="bg-white border border-slate-100 p-6 rounded-[2rem] premium-shadow flex flex-col justify-between min-h-[220px]">
|
|
<div>
|
|
<div className="flex items-center justify-between mb-4">
|
|
<span className="px-2.5 py-1 bg-cyan-50 text-cyan-600 border border-cyan-100 text-[8px] font-black uppercase tracking-wider rounded">
|
|
Special Event
|
|
</span>
|
|
<button
|
|
onClick={() => handleDeleteSpecialEvent(event.id)}
|
|
className="p-2 hover:bg-rose-50 text-slate-400 hover:text-rose-500 rounded-xl transition-all"
|
|
>
|
|
<Trash2 className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
|
|
<h3 className="text-base font-black text-text-dark tracking-tight mb-2 leading-tight">{event.title}</h3>
|
|
<p className="text-xs text-text-muted font-medium leading-relaxed mb-4 line-clamp-3">{event.description}</p>
|
|
</div>
|
|
|
|
<div className="border-t border-slate-50 pt-3 mt-auto flex items-center justify-between">
|
|
<a
|
|
href={event.link}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="text-[10px] font-black text-brand-indigo uppercase tracking-wider hover:underline flex items-center gap-1.5"
|
|
>
|
|
View Registry Link
|
|
<Globe className="w-3.5 h-3.5" />
|
|
</a>
|
|
</div>
|
|
</div>
|
|
))}
|
|
|
|
{specialEvents.length === 0 && (
|
|
<div className="col-span-full py-16 text-center text-slate-400 font-bold uppercase tracking-wider text-xs border-2 border-dashed border-slate-200 rounded-[2.5rem]">
|
|
No special events currently registered.
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Notice Dialog Modal */}
|
|
<AnimatePresence>
|
|
{isNoticeModalOpen && (
|
|
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4">
|
|
<motion.div
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
exit={{ opacity: 0 }}
|
|
onClick={() => setIsNoticeModalOpen(false)}
|
|
className="absolute inset-0 bg-slate-900/40 backdrop-blur-sm"
|
|
/>
|
|
<motion.div
|
|
initial={{ opacity: 0, scale: 0.9, y: 20 }}
|
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
|
exit={{ opacity: 0, scale: 0.9, y: 20 }}
|
|
className="relative w-full max-w-lg bg-white rounded-[2.5rem] premium-shadow overflow-hidden flex flex-col max-h-[90vh]"
|
|
>
|
|
<form onSubmit={handleAddNotice} className="flex flex-col overflow-hidden">
|
|
{/* Header */}
|
|
<div className="p-8 bg-brand-navy text-white flex justify-between items-start shrink-0">
|
|
<div>
|
|
<h3 className="text-2xl font-black tracking-tight">Post New Notice</h3>
|
|
<p className="text-white/60 text-xs font-medium mt-1">Configure campus notice board bulletin.</p>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={() => setIsNoticeModalOpen(false)}
|
|
className="p-2 hover:bg-white/10 rounded-xl transition-all"
|
|
>
|
|
<X className="w-6 h-6" />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Body */}
|
|
<div className="p-8 space-y-5 overflow-y-auto custom-scrollbar flex-1 max-h-[60vh]">
|
|
{/* Title */}
|
|
<div>
|
|
<label className="text-[10px] font-black uppercase tracking-widest text-text-muted mb-2 block">Notice Title</label>
|
|
<input
|
|
type="text"
|
|
required
|
|
value={noticeTitle}
|
|
onChange={e => setNoticeTitle(e.target.value)}
|
|
placeholder="Enter short, descriptive title"
|
|
className="w-full bg-slate-50 border-transparent rounded-xl py-3 px-4 text-xs font-bold focus:bg-white focus:border-brand-indigo outline-none transition-all"
|
|
/>
|
|
</div>
|
|
|
|
{/* Message */}
|
|
<div>
|
|
<label className="text-[10px] font-black uppercase tracking-widest text-text-muted mb-2 block">Notice Message</label>
|
|
<textarea
|
|
rows={3}
|
|
required
|
|
value={noticeMessage}
|
|
onChange={e => setNoticeMessage(e.target.value)}
|
|
placeholder="Write announcement body..."
|
|
className="w-full bg-slate-50 border-transparent rounded-xl py-3 px-4 text-xs font-bold focus:bg-white focus:border-brand-indigo outline-none transition-all"
|
|
/>
|
|
</div>
|
|
|
|
{/* Alert Type */}
|
|
<div>
|
|
<label className="text-[10px] font-black uppercase tracking-widest text-text-muted mb-2 block">Alert Casing</label>
|
|
<select
|
|
value={noticeType}
|
|
onChange={e => setNoticeType(e.target.value as any)}
|
|
className="w-full bg-slate-50 border-transparent rounded-xl py-3 px-4 text-xs font-bold focus:bg-white focus:border-brand-indigo outline-none transition-all appearance-none"
|
|
>
|
|
<option value="INFO">Information (INFO)</option>
|
|
<option value="URGENT">Urgent Announcement (URGENT)</option>
|
|
<option value="DELAY">Schedule Delay / Change (DELAY)</option>
|
|
</select>
|
|
</div>
|
|
|
|
{/* Image Dropzone */}
|
|
<div>
|
|
<label className="text-[10px] font-black uppercase tracking-widest text-text-muted mb-2 block">Notice Banner Image</label>
|
|
<div className="border-2 border-dashed border-slate-200 hover:border-brand-indigo/40 rounded-2xl p-6 bg-slate-50/50 flex flex-col items-center justify-center text-center relative group transition-colors">
|
|
<input
|
|
type="file"
|
|
accept="image/*"
|
|
onChange={handleImageUpload}
|
|
className="absolute inset-0 opacity-0 cursor-pointer w-full h-full z-10"
|
|
/>
|
|
|
|
{noticeImage ? (
|
|
<div className="space-y-3 z-20">
|
|
<img
|
|
src={noticeImage}
|
|
alt="Notice preview"
|
|
className="max-h-28 object-cover rounded-xl border border-slate-200"
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => setNoticeImage(null)}
|
|
className="text-[10px] font-black text-rose-500 hover:text-rose-700 uppercase tracking-widest block mx-auto transition-all"
|
|
>
|
|
Remove Banner
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-2 pointer-events-none">
|
|
<FileImage className="w-8 h-8 text-slate-300 mx-auto group-hover:text-brand-indigo/60 transition-colors" />
|
|
<div>
|
|
<span className="text-[10px] font-black text-brand-indigo uppercase tracking-wider">Upload banner image</span>
|
|
<p className="text-[9px] text-slate-400 font-bold mt-0.5">JPEG, PNG up to 1.5MB</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Footer */}
|
|
<div className="shrink-0 w-full">
|
|
<button
|
|
type="submit"
|
|
className="w-full bg-brand-navy hover:bg-[#0c365c] text-white py-5 font-black text-xs uppercase tracking-[0.2em] transition-all flex items-center justify-center"
|
|
>
|
|
Post Notice Bulletin
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</motion.div>
|
|
</div>
|
|
)}
|
|
</AnimatePresence>
|
|
|
|
{/* Special Event Dialog Modal */}
|
|
<AnimatePresence>
|
|
{isEventModalOpen && (
|
|
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4">
|
|
<motion.div
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
exit={{ opacity: 0 }}
|
|
onClick={() => setIsEventModalOpen(false)}
|
|
className="absolute inset-0 bg-slate-900/40 backdrop-blur-sm"
|
|
/>
|
|
<motion.div
|
|
initial={{ opacity: 0, scale: 0.9, y: 20 }}
|
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
|
exit={{ opacity: 0, scale: 0.9, y: 20 }}
|
|
className="relative w-full max-w-lg bg-white rounded-[2.5rem] premium-shadow overflow-hidden flex flex-col max-h-[90vh]"
|
|
>
|
|
<form onSubmit={handleAddSpecialEvent} className="flex flex-col overflow-hidden">
|
|
{/* Header */}
|
|
<div className="p-8 bg-brand-navy text-white flex justify-between items-start shrink-0">
|
|
<div>
|
|
<h3 className="text-2xl font-black tracking-tight">Add Special Event</h3>
|
|
<p className="text-white/60 text-xs font-medium mt-1">Configure special event registry entry.</p>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={() => setIsEventModalOpen(false)}
|
|
className="p-2 hover:bg-white/10 rounded-xl transition-all"
|
|
>
|
|
<X className="w-6 h-6" />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Body */}
|
|
<div className="p-8 space-y-5 overflow-y-auto custom-scrollbar flex-1 max-h-[60vh]">
|
|
{/* Title */}
|
|
<div>
|
|
<label className="text-[10px] font-black uppercase tracking-widest text-text-muted mb-2 block">Event Title</label>
|
|
<input
|
|
type="text"
|
|
required
|
|
value={eventTitle}
|
|
onChange={e => setEventTitle(e.target.value)}
|
|
placeholder="Hackathon, techfest, etc."
|
|
className="w-full bg-slate-50 border-transparent rounded-xl py-3 px-4 text-xs font-bold focus:bg-white focus:border-brand-indigo outline-none transition-all"
|
|
/>
|
|
</div>
|
|
|
|
{/* Description */}
|
|
<div>
|
|
<label className="text-[10px] font-black uppercase tracking-widest text-text-muted mb-2 block">Event Description</label>
|
|
<textarea
|
|
rows={3}
|
|
required
|
|
value={eventDesc}
|
|
onChange={e => setEventDesc(e.target.value)}
|
|
placeholder="Write brief description..."
|
|
className="w-full bg-slate-50 border-transparent rounded-xl py-3 px-4 text-xs font-bold focus:bg-white focus:border-brand-indigo outline-none transition-all"
|
|
/>
|
|
</div>
|
|
|
|
{/* Link */}
|
|
<div>
|
|
<label className="text-[10px] font-black uppercase tracking-widest text-text-muted mb-2 block">Registry External Link</label>
|
|
<input
|
|
type="url"
|
|
required
|
|
value={eventLink}
|
|
onChange={e => setEventLink(e.target.value)}
|
|
placeholder="https://example.com/registration"
|
|
className="w-full bg-slate-50 border-transparent rounded-xl py-3 px-4 text-xs font-bold focus:bg-white focus:border-brand-indigo outline-none transition-all"
|
|
/>
|
|
</div>
|
|
|
|
{/* Image Dropzone */}
|
|
<div>
|
|
<label className="text-[10px] font-black uppercase tracking-widest text-text-muted mb-2 block">Event Cover Image (Optional)</label>
|
|
<div className="border-2 border-dashed border-slate-200 hover:border-brand-indigo/40 rounded-2xl p-6 bg-slate-50/50 flex flex-col items-center justify-center text-center relative group transition-colors">
|
|
<input
|
|
type="file"
|
|
accept="image/*"
|
|
onChange={handleEventImageUpload}
|
|
className="absolute inset-0 opacity-0 cursor-pointer w-full h-full z-10"
|
|
/>
|
|
|
|
{eventImage ? (
|
|
<div className="space-y-3 z-20">
|
|
<img
|
|
src={eventImage}
|
|
alt="Event preview"
|
|
className="max-h-28 object-cover rounded-xl border border-slate-200"
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => setEventImage(null)}
|
|
className="text-[10px] font-black text-rose-500 hover:text-rose-700 uppercase tracking-widest block mx-auto transition-all"
|
|
>
|
|
Remove Cover Image
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-2 pointer-events-none">
|
|
<FileImage className="w-8 h-8 text-slate-300 mx-auto group-hover:text-brand-indigo/60 transition-colors" />
|
|
<div>
|
|
<span className="text-[10px] font-black text-brand-indigo uppercase tracking-wider">Upload cover image</span>
|
|
<p className="text-[9px] text-slate-400 font-bold mt-0.5">JPEG, PNG up to 1.5MB</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Footer */}
|
|
<div className="shrink-0 w-full">
|
|
<button
|
|
type="submit"
|
|
className="w-full bg-brand-navy hover:bg-[#0c365c] text-white py-5 font-black text-xs uppercase tracking-[0.2em] transition-all flex items-center justify-center"
|
|
>
|
|
Register Special Event
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</motion.div>
|
|
</div>
|
|
)}
|
|
</AnimatePresence>
|
|
</div>
|
|
);
|
|
};
|