753 lines
50 KiB
JavaScript
753 lines
50 KiB
JavaScript
import React, { useState } from 'react';
|
|
import { Head, router } from '@inertiajs/react';
|
|
import AdminLayout from '../../../Layouts/AdminLayout';
|
|
import {
|
|
Heart, Plus, Trash2, Send, CheckCircle2, Check, X,
|
|
Calendar, MapPin, Clock, Gift, Sparkles, Search,
|
|
ChevronDown, ChevronUp, Building, User, XCircle, Eye, BellRing
|
|
} from 'lucide-react';
|
|
|
|
const hoursList = [
|
|
'12:00', '12:30', '01:00', '01:30', '02:00', '02:30',
|
|
'03:00', '03:30', '04:00', '04:30', '05:00', '05:30',
|
|
'06:00', '06:30', '07:00', '07:30', '08:00', '08:30',
|
|
'09:00', '09:30', '10:00', '10:30', '11:00', '11:30'
|
|
];
|
|
|
|
export default function Announcements({ initialAnnouncements }) {
|
|
const [announcements, setAnnouncements] = useState(initialAnnouncements || []);
|
|
const [isFormOpen, setIsFormOpen] = useState(false);
|
|
const [searchQuery, setSearchQuery] = useState('');
|
|
const [statusFilter, setStatusFilter] = useState('All');
|
|
const [expandedCards, setExpandedCards] = useState({});
|
|
|
|
// Time picker states
|
|
const [startTimeHour, setStartTimeHour] = useState('09:00');
|
|
const [startTimeAmpm, setStartTimeAmpm] = useState('AM');
|
|
const [endTimeHour, setEndTimeHour] = useState('04:00');
|
|
const [endTimeAmpm, setEndTimeAmpm] = useState('PM');
|
|
const [isStartPickerOpen, setIsStartPickerOpen] = useState(false);
|
|
const [isEndPickerOpen, setIsEndPickerOpen] = useState(false);
|
|
|
|
const [newAnnouncement, setNewAnnouncement] = useState({
|
|
title: '',
|
|
content: '',
|
|
provided_items: '',
|
|
event_date: '',
|
|
event_time: '09:00 AM - 04:00 PM',
|
|
location_details: '',
|
|
location_pin: ''
|
|
});
|
|
const [errors, setErrors] = useState({});
|
|
const [toastMessage, setToastMessage] = useState(null);
|
|
const [rejectingAnnouncementId, setRejectingAnnouncementId] = useState(null);
|
|
const [rejectionRemarks, setRejectionRemarks] = useState('');
|
|
|
|
const showToast = (message) => {
|
|
setToastMessage(message);
|
|
setTimeout(() => setToastMessage(null), 3000);
|
|
};
|
|
|
|
const handleCreate = (e) => {
|
|
e.preventDefault();
|
|
let newErrors = {};
|
|
if (!newAnnouncement.title.trim()) newErrors.title = 'Title is required';
|
|
if (!newAnnouncement.content.trim()) newErrors.content = 'Description is required';
|
|
if (!newAnnouncement.provided_items.trim()) newErrors.provided_items = 'Provided items is required';
|
|
if (!newAnnouncement.event_date.trim()) newErrors.event_date = 'Event date is required';
|
|
if (!newAnnouncement.event_time.trim()) newErrors.event_time = 'Event time is required';
|
|
if (!newAnnouncement.location_details.trim()) newErrors.location_details = 'Location details is required';
|
|
if (!newAnnouncement.location_pin.trim()) {
|
|
newErrors.location_pin = 'Location pin URL is required';
|
|
} else if (!newAnnouncement.location_pin.startsWith('http://') && !newAnnouncement.location_pin.startsWith('https://')) {
|
|
newErrors.location_pin = 'Must be a valid URL';
|
|
}
|
|
|
|
if (Object.keys(newErrors).length > 0) {
|
|
setErrors(newErrors);
|
|
return;
|
|
}
|
|
|
|
router.post(route('admin.announcements.store'), {
|
|
title: newAnnouncement.title,
|
|
content: newAnnouncement.content,
|
|
provided_items: newAnnouncement.provided_items,
|
|
event_date: newAnnouncement.event_date,
|
|
event_time: newAnnouncement.event_time,
|
|
location_details: newAnnouncement.location_details,
|
|
location_pin: newAnnouncement.location_pin
|
|
}, {
|
|
onSuccess: () => {
|
|
setNewAnnouncement({
|
|
title: '',
|
|
content: '',
|
|
provided_items: '',
|
|
event_date: '',
|
|
event_time: '09:00 AM - 04:00 PM',
|
|
location_details: '',
|
|
location_pin: ''
|
|
});
|
|
setStartTimeHour('09:00');
|
|
setStartTimeAmpm('AM');
|
|
setEndTimeHour('04:00');
|
|
setEndTimeAmpm('PM');
|
|
setErrors({});
|
|
setIsFormOpen(false);
|
|
showToast('Charity Event created successfully');
|
|
setTimeout(() => window.location.reload(), 500);
|
|
}
|
|
});
|
|
};
|
|
|
|
const updateEventTime = (sh, sa, eh, ea) => {
|
|
setNewAnnouncement(prev => ({
|
|
...prev,
|
|
event_time: `${sh} ${sa} - ${eh} ${ea}`
|
|
}));
|
|
};
|
|
|
|
const handleApprove = (id) => {
|
|
router.post(route('admin.announcements.approve', id), {}, {
|
|
onSuccess: () => {
|
|
showToast('Charity Event approved successfully');
|
|
setTimeout(() => window.location.reload(), 500);
|
|
}
|
|
});
|
|
};
|
|
|
|
const handleReject = (id) => {
|
|
setRejectingAnnouncementId(id);
|
|
setRejectionRemarks('');
|
|
};
|
|
|
|
const handleRejectSubmit = (e) => {
|
|
e.preventDefault();
|
|
if (!rejectionRemarks.trim()) return;
|
|
|
|
router.post(route('admin.announcements.reject', rejectingAnnouncementId), {
|
|
remarks: rejectionRemarks
|
|
}, {
|
|
onSuccess: () => {
|
|
setRejectingAnnouncementId(null);
|
|
setRejectionRemarks('');
|
|
showToast('Charity Event rejected successfully');
|
|
setTimeout(() => window.location.reload(), 500);
|
|
}
|
|
});
|
|
};
|
|
|
|
const handleDelete = (id) => {
|
|
if (confirm('Are you sure you want to delete this charity event?')) {
|
|
router.delete(route('admin.announcements.delete', id), {
|
|
onSuccess: () => {
|
|
showToast('Charity Event deleted');
|
|
setTimeout(() => window.location.reload(), 500);
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
const toggleExpand = (id) => {
|
|
setExpandedCards(prev => ({
|
|
...prev,
|
|
[id]: !prev[id]
|
|
}));
|
|
};
|
|
|
|
// Calculate dynamic statistics
|
|
const totalDrives = announcements.length;
|
|
const pendingCount = announcements.filter(a => a.status === 'pending').length;
|
|
const approvedCount = announcements.filter(a => a.status === 'approved').length;
|
|
const rejectedCount = announcements.filter(a => a.status === 'rejected').length;
|
|
|
|
// Filter announcements
|
|
const filteredAnnouncements = announcements.filter(ann => {
|
|
const matchesSearch =
|
|
ann.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
ann.content.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
(ann.posted_by && ann.posted_by.toLowerCase().includes(searchQuery.toLowerCase())) ||
|
|
(ann.organization && ann.organization.toLowerCase().includes(searchQuery.toLowerCase())) ||
|
|
(ann.charityDetails?.provided_items && ann.charityDetails.provided_items.toLowerCase().includes(searchQuery.toLowerCase()));
|
|
|
|
const matchesStatus = statusFilter === 'All' || ann.status === statusFilter.toLowerCase();
|
|
|
|
return matchesSearch && matchesStatus;
|
|
});
|
|
|
|
return (
|
|
<AdminLayout title="Charity Events & Drives">
|
|
<Head title="Charity Events - Admin Portal" />
|
|
|
|
{/* Toast Notification */}
|
|
{toastMessage && (
|
|
<div className="fixed bottom-4 right-4 bg-slate-900 text-white px-6 py-3 rounded-xl shadow-2xl flex items-center space-x-3 z-50 animate-in slide-in-from-bottom-5">
|
|
<CheckCircle2 className="w-5 h-5 text-emerald-400" />
|
|
<span className="font-medium text-sm">{toastMessage}</span>
|
|
</div>
|
|
)}
|
|
|
|
<div className="space-y-6 select-none max-w-6xl mx-auto pb-12">
|
|
|
|
{/* Upper Header section */}
|
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
|
<div>
|
|
<h1 className="text-2xl font-black text-slate-900 tracking-tight">Charity Events & Drives</h1>
|
|
<p className="text-sm text-slate-500 font-medium">Manage, approve, or launch community charity initiatives.</p>
|
|
</div>
|
|
<button
|
|
onClick={() => setIsFormOpen(true)}
|
|
className="bg-[#0F6E56] hover:bg-[#0b523f] text-white px-5 py-2.5 rounded-xl font-bold text-sm flex items-center justify-center space-x-2 transition-all shadow-sm cursor-pointer border-none self-start sm:self-auto hover:scale-[1.02] active:scale-[0.98]"
|
|
>
|
|
<Plus className="w-4 h-4" />
|
|
<span>New Charity Event</span>
|
|
</button>
|
|
</div>
|
|
|
|
{/* Dashboard Stats Overview (Compact) */}
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
<div className="bg-white p-4 rounded-xl border border-slate-200 shadow-sm flex items-center space-x-3.5">
|
|
<div className="w-10 h-10 rounded-lg bg-teal-50 flex items-center justify-center text-teal-600 shrink-0">
|
|
<Heart className="w-5 h-5 fill-teal-500 text-teal-500" />
|
|
</div>
|
|
<div>
|
|
<div className="text-xs text-slate-400 font-bold uppercase tracking-wider">Total Drives</div>
|
|
<div className="text-xl font-black text-slate-800">{totalDrives}</div>
|
|
</div>
|
|
</div>
|
|
<div className="bg-white p-4 rounded-xl border border-slate-200 shadow-sm flex items-center space-x-3.5">
|
|
<div className="w-10 h-10 rounded-lg bg-amber-50 flex items-center justify-center text-amber-600 shrink-0">
|
|
<Clock className="w-5 h-5 text-amber-500" />
|
|
</div>
|
|
<div>
|
|
<div className="text-xs text-slate-400 font-bold uppercase tracking-wider">Pending</div>
|
|
<div className="text-xl font-black text-slate-800">{pendingCount}</div>
|
|
</div>
|
|
</div>
|
|
<div className="bg-white p-4 rounded-xl border border-slate-200 shadow-sm flex items-center space-x-3.5">
|
|
<div className="w-10 h-10 rounded-lg bg-emerald-50 flex items-center justify-center text-emerald-600 shrink-0">
|
|
<CheckCircle2 className="w-5 h-5 text-emerald-500" />
|
|
</div>
|
|
<div>
|
|
<div className="text-xs text-slate-400 font-bold uppercase tracking-wider">Approved</div>
|
|
<div className="text-xl font-black text-slate-800">{approvedCount}</div>
|
|
</div>
|
|
</div>
|
|
<div className="bg-white p-4 rounded-xl border border-slate-200 shadow-sm flex items-center space-x-3.5">
|
|
<div className="w-10 h-10 rounded-lg bg-rose-50 flex items-center justify-center text-rose-600 shrink-0">
|
|
<XCircle className="w-5 h-5 text-rose-500" />
|
|
</div>
|
|
<div>
|
|
<div className="text-xs text-slate-400 font-bold uppercase tracking-wider">Rejected</div>
|
|
<div className="text-xl font-black text-slate-800">{rejectedCount}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Search & Filtering Control Bar */}
|
|
<div className="bg-white p-3 rounded-xl border border-slate-200 shadow-sm flex flex-col md:flex-row md:items-center justify-between gap-4">
|
|
{/* Search Field */}
|
|
<div className="relative flex-1 max-w-md">
|
|
<Search className="w-4 h-4 text-slate-400 absolute left-3.5 top-1/2 -translate-y-1/2" />
|
|
<input
|
|
type="text"
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
placeholder="Search events by title, description, or sponsor..."
|
|
className="w-full pl-10 pr-4 py-2 text-sm bg-slate-50 border border-slate-200 rounded-lg outline-none focus:bg-white focus:border-[#0F6E56] focus:ring-2 focus:ring-[#0F6E56]/10 transition-all"
|
|
/>
|
|
{searchQuery && (
|
|
<button
|
|
type="button"
|
|
onClick={() => setSearchQuery('')}
|
|
className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600 text-xs bg-slate-200 hover:bg-slate-300 w-4 h-4 rounded-full flex items-center justify-center border-none cursor-pointer"
|
|
>
|
|
<X className="w-2.5 h-2.5" />
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
{/* Filter Tabs */}
|
|
<div className="flex items-center space-x-1.5 overflow-x-auto self-start md:self-auto">
|
|
{['All', 'Pending', 'Approved', 'Rejected'].map((tab) => (
|
|
<button
|
|
key={tab}
|
|
type="button"
|
|
onClick={() => setStatusFilter(tab)}
|
|
className={`px-4 py-1.5 rounded-lg text-xs font-bold transition-all cursor-pointer border border-solid border-transparent ${
|
|
statusFilter === tab
|
|
? 'bg-[#0F6E56] text-white shadow-sm'
|
|
: 'text-slate-600 hover:bg-slate-100 hover:text-slate-900'
|
|
}`}
|
|
>
|
|
{tab}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Event Creation Modal */}
|
|
{isFormOpen && (
|
|
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-50 animate-in fade-in">
|
|
<div className="bg-white p-6 rounded-2xl border border-slate-200 shadow-2xl w-full max-w-lg animate-in slide-in-from-bottom-4 max-h-[90vh] overflow-y-auto">
|
|
<div className="flex items-center justify-between mb-4 pb-2 border-b border-slate-100">
|
|
<div className="flex items-center space-x-2 text-[#0F6E56] font-bold">
|
|
<Heart className="w-5 h-5 fill-[#0F6E56]" />
|
|
<h2>Compose Charity Event</h2>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={() => { setIsFormOpen(false); setErrors({}); }}
|
|
className="p-1 rounded-lg text-slate-400 hover:text-slate-600 hover:bg-slate-100 border-none bg-transparent cursor-pointer"
|
|
>
|
|
<X className="w-5 h-5" />
|
|
</button>
|
|
</div>
|
|
<form onSubmit={handleCreate} className="space-y-4">
|
|
<div>
|
|
<label className="block text-xs font-bold text-slate-600 mb-1">Event Title</label>
|
|
<input
|
|
type="text"
|
|
value={newAnnouncement.title}
|
|
onChange={(e) => setNewAnnouncement({ ...newAnnouncement, title: e.target.value })}
|
|
className={`w-full px-4 py-2.5 rounded-xl border ${errors.title ? 'border-red-500' : 'border-slate-300'} text-sm focus:ring-2 focus:ring-[#0F6E56]/20 focus:border-[#0F6E56] outline-none transition-all`}
|
|
placeholder="e.g. Free Dental checkup by Emirates Charity"
|
|
/>
|
|
{errors.title && <p className="text-red-500 text-xs mt-1 font-semibold">{errors.title}</p>}
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
|
<div>
|
|
<label className="block text-xs font-bold text-slate-600 mb-1">Event Date</label>
|
|
<input
|
|
type="date"
|
|
value={newAnnouncement.event_date}
|
|
onChange={(e) => setNewAnnouncement({ ...newAnnouncement, event_date: e.target.value })}
|
|
className={`w-full h-11 px-3.5 rounded-xl border ${errors.event_date ? 'border-red-500' : 'border-slate-300'} text-xs focus:ring-2 focus:ring-[#0F6E56]/20 focus:border-[#0F6E56] outline-none transition-all`}
|
|
/>
|
|
{errors.event_date && <p className="text-red-500 text-[10px] mt-1 font-semibold">{errors.event_date}</p>}
|
|
</div>
|
|
|
|
<div className="relative">
|
|
{isStartPickerOpen && (
|
|
<div className="fixed inset-0 z-[45] bg-transparent" onClick={() => setIsStartPickerOpen(false)} />
|
|
)}
|
|
<div className="relative z-50">
|
|
<label className="block text-xs font-bold text-slate-600 mb-1">Start Time</label>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
setIsStartPickerOpen(!isStartPickerOpen);
|
|
setIsEndPickerOpen(false);
|
|
}}
|
|
className={`w-full h-11 flex items-center justify-between px-3.5 bg-white border ${
|
|
isStartPickerOpen ? 'border-blue-500 ring-2 ring-blue-500/10' : 'border-slate-300'
|
|
} rounded-xl hover:border-blue-500 transition-all cursor-pointer text-left`}
|
|
>
|
|
<div className="flex items-center space-x-2 min-w-0">
|
|
<Clock className="w-4 h-4 text-slate-400 shrink-0" />
|
|
<div className="flex flex-col justify-center min-w-0 leading-tight">
|
|
<div className="text-[8px] text-blue-500 font-bold uppercase tracking-wider">Start with</div>
|
|
<div className="text-slate-800 text-xs font-bold truncate">{startTimeHour} {startTimeAmpm}</div>
|
|
</div>
|
|
</div>
|
|
<ChevronDown className="w-3.5 h-3.5 text-slate-400 shrink-0" />
|
|
</button>
|
|
|
|
{isStartPickerOpen && (
|
|
<div className="absolute top-full mt-1.5 left-0 bg-white border border-slate-200 shadow-xl rounded-xl p-2 z-50 flex space-x-2.5 w-48 animate-in fade-in slide-in-from-top-2">
|
|
<div className="flex-1 max-h-40 overflow-y-auto pr-1 space-y-1 scrollbar-thin">
|
|
{hoursList.map(h => (
|
|
<button
|
|
key={h}
|
|
type="button"
|
|
onClick={() => {
|
|
setStartTimeHour(h);
|
|
setIsStartPickerOpen(false);
|
|
updateEventTime(h, startTimeAmpm, endTimeHour, endTimeAmpm);
|
|
}}
|
|
className={`w-full text-left px-2 py-1 rounded text-xs font-semibold transition-colors ${
|
|
startTimeHour === h
|
|
? 'bg-blue-50 text-blue-600'
|
|
: 'text-slate-600 hover:bg-slate-50'
|
|
}`}
|
|
>
|
|
{h}
|
|
</button>
|
|
))}
|
|
</div>
|
|
<div className="flex flex-col space-y-1 justify-center border-l border-slate-100 pl-2">
|
|
{['AM', 'PM'].map(ampm => (
|
|
<button
|
|
key={ampm}
|
|
type="button"
|
|
onClick={() => {
|
|
setStartTimeAmpm(ampm);
|
|
updateEventTime(startTimeHour, ampm, endTimeHour, endTimeAmpm);
|
|
}}
|
|
className={`px-2 py-1 rounded text-[10px] font-black transition-all border border-solid ${
|
|
startTimeAmpm === ampm
|
|
? 'bg-blue-600 border-blue-600 text-white shadow-sm'
|
|
: 'border-slate-200 text-slate-400 hover:bg-slate-50'
|
|
}`}
|
|
>
|
|
{ampm}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="relative">
|
|
{isEndPickerOpen && (
|
|
<div className="fixed inset-0 z-[45] bg-transparent" onClick={() => setIsEndPickerOpen(false)} />
|
|
)}
|
|
<div className="relative z-50">
|
|
<label className="block text-xs font-bold text-slate-600 mb-1">End Time</label>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
setIsEndPickerOpen(!isEndPickerOpen);
|
|
setIsStartPickerOpen(false);
|
|
}}
|
|
className={`w-full h-11 flex items-center justify-between px-3.5 bg-white border ${
|
|
isEndPickerOpen ? 'border-blue-500 ring-2 ring-blue-500/10' : 'border-slate-300'
|
|
} rounded-xl hover:border-blue-500 transition-all cursor-pointer text-left`}
|
|
>
|
|
<div className="flex items-center space-x-2 min-w-0">
|
|
<Clock className="w-4 h-4 text-slate-400 shrink-0" />
|
|
<div className="flex flex-col justify-center min-w-0 leading-tight">
|
|
<div className="text-[8px] text-slate-500 font-bold uppercase tracking-wider">End with</div>
|
|
<div className="text-slate-800 text-xs font-bold truncate">{endTimeHour} {endTimeAmpm}</div>
|
|
</div>
|
|
</div>
|
|
<ChevronDown className="w-3.5 h-3.5 text-slate-400 shrink-0" />
|
|
</button>
|
|
|
|
{isEndPickerOpen && (
|
|
<div className="absolute top-full mt-1.5 right-0 bg-white border border-slate-200 shadow-xl rounded-xl p-2 z-50 flex space-x-2.5 w-48 animate-in fade-in slide-in-from-top-2">
|
|
<div className="flex-1 max-h-40 overflow-y-auto pr-1 space-y-1 scrollbar-thin">
|
|
{hoursList.map(h => (
|
|
<button
|
|
key={h}
|
|
type="button"
|
|
onClick={() => {
|
|
setEndTimeHour(h);
|
|
setIsEndPickerOpen(false);
|
|
updateEventTime(startTimeHour, startTimeAmpm, h, endTimeAmpm);
|
|
}}
|
|
className={`w-full text-left px-2 py-1 rounded text-xs font-semibold transition-colors ${
|
|
endTimeHour === h
|
|
? 'bg-blue-50 text-blue-600'
|
|
: 'text-slate-600 hover:bg-slate-50'
|
|
}`}
|
|
>
|
|
{h}
|
|
</button>
|
|
))}
|
|
</div>
|
|
<div className="flex flex-col space-y-1 justify-center border-l border-slate-100 pl-2">
|
|
{['AM', 'PM'].map(ampm => (
|
|
<button
|
|
key={ampm}
|
|
type="button"
|
|
onClick={() => {
|
|
setEndTimeAmpm(ampm);
|
|
updateEventTime(startTimeHour, startTimeAmpm, endTimeHour, ampm);
|
|
}}
|
|
className={`px-2 py-1 rounded text-[10px] font-black transition-all border border-solid ${
|
|
endTimeAmpm === ampm
|
|
? 'bg-blue-600 border-blue-600 text-white shadow-sm'
|
|
: 'border-slate-200 text-slate-400 hover:bg-slate-50'
|
|
}`}
|
|
>
|
|
{ampm}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-xs font-bold text-slate-600 mb-1">What is Being Provided</label>
|
|
<input
|
|
type="text"
|
|
value={newAnnouncement.provided_items}
|
|
onChange={(e) => setNewAnnouncement({ ...newAnnouncement, provided_items: e.target.value })}
|
|
className={`w-full px-4 py-2.5 rounded-xl border ${errors.provided_items ? 'border-red-500' : 'border-slate-300'} text-sm focus:ring-2 focus:ring-[#0F6E56]/20 focus:border-[#0F6E56] outline-none transition-all`}
|
|
placeholder="e.g. Free Medical Checks & Food Supplies"
|
|
/>
|
|
{errors.provided_items && <p className="text-red-500 text-xs mt-1 font-semibold">{errors.provided_items}</p>}
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 gap-3">
|
|
<div>
|
|
<label className="block text-xs font-bold text-slate-600 mb-1">Location Details</label>
|
|
<input
|
|
type="text"
|
|
value={newAnnouncement.location_details}
|
|
onChange={(e) => setNewAnnouncement({ ...newAnnouncement, location_details: e.target.value })}
|
|
className={`w-full px-4 py-2.5 rounded-xl border ${errors.location_details ? 'border-red-500' : 'border-slate-300'} text-sm focus:ring-2 focus:ring-[#0F6E56]/20 focus:border-[#0F6E56] outline-none transition-all`}
|
|
placeholder="e.g. Al Quoz Community Center, Dubai"
|
|
/>
|
|
{errors.location_details && <p className="text-red-500 text-xs mt-1 font-semibold">{errors.location_details}</p>}
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs font-bold text-slate-600 mb-1">Location Pin URL</label>
|
|
<input
|
|
type="text"
|
|
value={newAnnouncement.location_pin}
|
|
onChange={(e) => setNewAnnouncement({ ...newAnnouncement, location_pin: e.target.value })}
|
|
className={`w-full px-4 py-2.5 rounded-xl border ${errors.location_pin ? 'border-red-500' : 'border-slate-300'} text-sm focus:ring-2 focus:ring-[#0F6E56]/20 focus:border-[#0F6E56] outline-none transition-all`}
|
|
placeholder="e.g. https://maps.app.goo.gl/xyz"
|
|
/>
|
|
{errors.location_pin && <p className="text-red-500 text-xs mt-1 font-semibold">{errors.location_pin}</p>}
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-xs font-bold text-slate-600 mb-1">Description & Instructions</label>
|
|
<textarea
|
|
rows="3"
|
|
value={newAnnouncement.content}
|
|
onChange={(e) => setNewAnnouncement({ ...newAnnouncement, content: e.target.value })}
|
|
className={`w-full px-4 py-3 rounded-xl border ${errors.content ? 'border-red-500' : 'border-slate-300'} text-sm focus:ring-2 focus:ring-[#0F6E56]/20 focus:border-[#0F6E56] outline-none transition-all resize-none`}
|
|
placeholder="Type event details and instructions here..."
|
|
></textarea>
|
|
{errors.content && <p className="text-red-500 text-xs mt-1 font-semibold">{errors.content}</p>}
|
|
</div>
|
|
|
|
<div className="flex items-center justify-end space-x-3 pt-2 border-t border-slate-100">
|
|
<button
|
|
type="button"
|
|
onClick={() => { setIsFormOpen(false); setErrors({}); }}
|
|
className="px-5 py-2.5 rounded-xl text-sm font-semibold text-slate-600 hover:bg-slate-100 transition-colors cursor-pointer border-none"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
className="bg-emerald-600 hover:bg-emerald-700 text-white px-6 py-2.5 rounded-xl text-sm font-bold flex items-center space-x-2 transition-colors shadow-sm cursor-pointer border-none"
|
|
>
|
|
<Send className="w-4 h-4" />
|
|
<span>Publish Event</span>
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Rejection remarks modal */}
|
|
{rejectingAnnouncementId && (
|
|
<div className="fixed inset-0 bg-slate-900/60 backdrop-blur-sm flex items-center justify-center p-4 z-50 animate-fade-in">
|
|
<div className="bg-white rounded-3xl max-w-md w-full p-6 space-y-4 shadow-2xl border border-slate-100 transform scale-100 transition-all font-sans">
|
|
<div className="flex items-center justify-between border-b border-slate-100 pb-3">
|
|
<h3 className="text-base font-black text-slate-900 uppercase tracking-tight flex items-center gap-2">
|
|
<XCircle className="w-5 h-5 text-rose-500" />
|
|
<span>Reject Charity Event</span>
|
|
</h3>
|
|
<button
|
|
onClick={() => setRejectingAnnouncementId(null)}
|
|
className="text-slate-400 hover:text-slate-600 bg-transparent border-none cursor-pointer"
|
|
>
|
|
<X className="w-5 h-5" />
|
|
</button>
|
|
</div>
|
|
|
|
<form onSubmit={handleRejectSubmit} className="space-y-4">
|
|
<div className="space-y-2">
|
|
<label className="text-[10px] font-black text-slate-500 uppercase tracking-widest ml-1">
|
|
Reason for Rejection / Remarks
|
|
</label>
|
|
<textarea
|
|
required
|
|
rows="4"
|
|
value={rejectionRemarks}
|
|
onChange={(e) => setRejectionRemarks(e.target.value)}
|
|
className="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-2xl text-xs font-bold focus:ring-2 focus:ring-rose-500/20 focus:border-rose-500 outline-none resize-none transition-all"
|
|
placeholder="Please provide a reason for rejecting this event..."
|
|
></textarea>
|
|
</div>
|
|
|
|
<div className="flex space-x-2 pt-2">
|
|
<button
|
|
type="button"
|
|
onClick={() => setRejectingAnnouncementId(null)}
|
|
className="flex-1 bg-slate-100 hover:bg-slate-200 text-slate-700 py-3 rounded-xl text-xs font-black uppercase tracking-widest transition-colors cursor-pointer border-none"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
disabled={!rejectionRemarks.trim()}
|
|
className="flex-1 bg-rose-600 hover:bg-rose-700 text-white py-3 rounded-xl text-xs font-black uppercase tracking-widest transition-colors cursor-pointer border-none disabled:opacity-50 disabled:cursor-not-allowed shadow-lg shadow-rose-200"
|
|
>
|
|
Reject Event
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Compact Grid of Events */}
|
|
{filteredAnnouncements.length === 0 ? (
|
|
<div className="text-center py-16 bg-white rounded-2xl border border-slate-200 shadow-sm text-slate-500">
|
|
<Heart className="w-12 h-12 text-slate-300 mx-auto mb-3 fill-slate-100" />
|
|
<p className="font-bold text-sm">No charity events match your criteria.</p>
|
|
</div>
|
|
) : (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
{filteredAnnouncements.map((ann) => {
|
|
const details = ann.charityDetails;
|
|
const isExpanded = !!expandedCards[ann.id];
|
|
|
|
// Color accents based on status
|
|
const statusColorClass =
|
|
ann.status === 'approved' ? 'border-l-emerald-500' :
|
|
ann.status === 'rejected' ? 'border-l-rose-500' :
|
|
'border-l-amber-500';
|
|
|
|
return (
|
|
<div
|
|
key={ann.id}
|
|
className={`bg-white rounded-xl border border-slate-200 border-l-4 ${statusColorClass} shadow-sm hover:shadow-md transition-all flex flex-col justify-between`}
|
|
>
|
|
{/* Top Card Area */}
|
|
<div className="p-4 space-y-3">
|
|
<div className="flex items-start justify-between gap-2">
|
|
<div className="space-y-1 flex-1 min-w-0">
|
|
<div className="flex items-center space-x-2 flex-wrap gap-y-1">
|
|
<h3 className="font-bold text-sm text-slate-800 tracking-tight flex items-center gap-1 min-w-0">
|
|
<Sparkles className="w-3.5 h-3.5 text-rose-500 fill-rose-50 shrink-0" />
|
|
<span className="truncate block" title={ann.title}>{ann.title}</span>
|
|
</h3>
|
|
<span className={`px-2 py-0.5 rounded text-[8px] font-extrabold uppercase tracking-wider shrink-0 ${
|
|
ann.status === 'approved' ? 'bg-emerald-50 text-emerald-700 border border-solid border-emerald-100' :
|
|
ann.status === 'rejected' ? 'bg-rose-50 text-rose-700 border border-solid border-rose-100' :
|
|
'bg-amber-50 text-amber-700 border border-solid border-amber-100'
|
|
}`}>
|
|
{ann.status}
|
|
</span>
|
|
|
|
</div>
|
|
<div className="flex items-center text-[10px] text-slate-400 font-bold whitespace-nowrap overflow-hidden text-ellipsis">
|
|
<Building className="w-3 h-3 mr-1 text-slate-400 shrink-0" />
|
|
<span className="truncate max-w-[120px]" title={ann.organization}>{ann.organization}</span>
|
|
<span className="mx-1">•</span>
|
|
<User className="w-3 h-3 mr-1 text-slate-400 shrink-0" />
|
|
<span className="truncate max-w-[100px]" title={ann.posted_by}>{ann.posted_by}</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Action Control Buttons */}
|
|
<div className="flex items-center space-x-1 shrink-0">
|
|
{ann.status === 'pending' && (
|
|
<div className="flex items-center space-x-1 bg-slate-50 p-0.5 rounded-lg border border-slate-200">
|
|
<button
|
|
type="button"
|
|
onClick={() => handleApprove(ann.id)}
|
|
className="p-1 text-emerald-600 hover:bg-emerald-50 rounded transition-colors cursor-pointer border-none bg-transparent"
|
|
title="Approve"
|
|
>
|
|
<Check className="w-4 h-4" />
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => handleReject(ann.id)}
|
|
className="p-1 text-rose-600 hover:bg-rose-50 rounded transition-colors cursor-pointer border-none bg-transparent"
|
|
title="Reject"
|
|
>
|
|
<X className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
)}
|
|
<button
|
|
type="button"
|
|
onClick={() => handleDelete(ann.id)}
|
|
className="p-1 text-slate-400 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors cursor-pointer border-none bg-transparent"
|
|
title="Delete"
|
|
>
|
|
<Trash2 className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Collapsible Content */}
|
|
<div className="space-y-2 text-xs">
|
|
<p className={`text-slate-600 leading-relaxed ${isExpanded ? '' : 'line-clamp-2'}`}>
|
|
{ann.content}
|
|
</p>
|
|
|
|
{ann.content.length > 90 && (
|
|
<button
|
|
type="button"
|
|
onClick={() => toggleExpand(ann.id)}
|
|
className="text-[#0F6E56] hover:underline font-bold flex items-center space-x-0.5 border-none bg-transparent cursor-pointer p-0 text-[10px]"
|
|
>
|
|
<span>{isExpanded ? 'Show Less' : 'Read Full Description'}</span>
|
|
{isExpanded ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />}
|
|
</button>
|
|
)}
|
|
{ann.status === 'rejected' && ann.remarks && (
|
|
<div className="bg-rose-50 border border-rose-100 rounded-xl p-2.5 text-rose-800 text-[11px] font-semibold mt-1.5 leading-relaxed">
|
|
<strong>Remarks:</strong> {ann.remarks}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Details metadata row (compact icons) */}
|
|
{details && (
|
|
<div className="pt-2.5 border-t border-slate-100 grid grid-cols-3 gap-2 text-[10px] text-slate-500 font-bold">
|
|
<div className="flex items-center space-x-1.5 min-w-0">
|
|
<Gift className="w-3.5 h-3.5 text-rose-500 fill-rose-50 shrink-0" />
|
|
<span className="truncate text-slate-700" title={details.provided_items}>
|
|
{details.provided_items}
|
|
</span>
|
|
</div>
|
|
<div className="flex items-center space-x-1.5 min-w-0">
|
|
<Calendar className="w-3.5 h-3.5 text-amber-500 shrink-0" />
|
|
<span className="truncate text-slate-700" title={`${details.event_date} ${details.event_time}`}>
|
|
{details.event_date}
|
|
</span>
|
|
</div>
|
|
<div className="flex items-center space-x-1.5 min-w-0">
|
|
<MapPin className="w-3.5 h-3.5 text-blue-500 shrink-0" />
|
|
<span className="truncate text-slate-700" title={details.location_details}>
|
|
{details.location_details}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Bottom Card Footer */}
|
|
<div className="px-4 py-2 bg-slate-50 rounded-b-xl border-t border-slate-100 flex items-center justify-between text-[9px] text-slate-400 font-bold">
|
|
<span>Published: {ann.created_at}</span>
|
|
{details?.location_pin && (
|
|
<a
|
|
href={details.location_pin}
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
className="text-[#0F6E56] hover:underline flex items-center space-x-0.5"
|
|
>
|
|
<span>View on Map</span>
|
|
<MapPin className="w-2.5 h-2.5" />
|
|
</a>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</AdminLayout>
|
|
);
|
|
}
|