Update frontend components and integrate firebase backend
This commit is contained in:
@@ -55,7 +55,11 @@ const AppContent: React.FC = () => {
|
||||
...event,
|
||||
eventName: event.title,
|
||||
venue: event.location,
|
||||
category: event.category,
|
||||
eventType: event.type,
|
||||
socialProfile: event.guestSocialProfile,
|
||||
maxParticipants: event["total capacity"] || (event.maxParticipants ?? ''),
|
||||
sponsors: event.sponsors ?? [],
|
||||
isEditMode: true
|
||||
});
|
||||
setActiveItem('propose');
|
||||
|
||||
@@ -4,12 +4,10 @@ import { motion } from 'framer-motion';
|
||||
import {
|
||||
Calendar,
|
||||
MapPin,
|
||||
Building2,
|
||||
Clock,
|
||||
ChevronRight,
|
||||
Filter,
|
||||
Search,
|
||||
MoreVertical,
|
||||
Trash2,
|
||||
FileSpreadsheet
|
||||
} from 'lucide-react';
|
||||
import { cn } from '../../lib/utils';
|
||||
@@ -54,6 +52,21 @@ export const AllEvents: React.FC<AllEventsProps> = ({ onEditEvent }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const deleteEvent = async (eventId: number) => {
|
||||
if (!window.confirm('Are you sure you want to permanently delete this event? This action cannot be undone.')) return;
|
||||
try {
|
||||
const res = await fetch(`${API_BASE_URL}/api/events/${eventId}`, { method: 'DELETE' });
|
||||
if (res.ok) {
|
||||
setEvents(prev => prev.filter(e => e.id !== eventId));
|
||||
} else {
|
||||
alert('Failed to delete event. Please try again.');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Delete failed:', err);
|
||||
alert('An error occurred while deleting the event.');
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = (status: Event['status']) => {
|
||||
switch (status) {
|
||||
case 'REQUESTED': return 'bg-brand-glow text-brand-indigo border-brand-indigo/20';
|
||||
@@ -204,9 +217,15 @@ export const AllEvents: React.FC<AllEventsProps> = ({ onEditEvent }) => {
|
||||
Edit
|
||||
</button>
|
||||
)}
|
||||
<button className="p-2 rounded-lg hover:bg-white hover:premium-shadow transition-all text-slate-300 hover:text-brand-indigo">
|
||||
<MoreVertical className="w-5 h-5" />
|
||||
</button>
|
||||
{user?.role === 'ADMIN' && (
|
||||
<button
|
||||
onClick={() => deleteEvent(event.id)}
|
||||
className="p-2 rounded-lg hover:bg-red-50 transition-all text-slate-300 hover:text-red-500"
|
||||
title="Delete Event"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</motion.tr>
|
||||
|
||||
@@ -20,7 +20,8 @@ import {
|
||||
Building2,
|
||||
Users,
|
||||
Wallet,
|
||||
Ticket
|
||||
Ticket,
|
||||
Layers
|
||||
} from 'lucide-react';
|
||||
import { cn } from '../../lib/utils';
|
||||
import { format } from 'date-fns';
|
||||
@@ -41,13 +42,27 @@ interface Event {
|
||||
institution: string;
|
||||
category: string;
|
||||
academicYears: string[];
|
||||
targetDepartments?: string[];
|
||||
targetedSections?: string[];
|
||||
proposer?: {
|
||||
fullName: string;
|
||||
email?: string;
|
||||
department?: string;
|
||||
role?: string;
|
||||
};
|
||||
description?: string;
|
||||
sponsors?: string[];
|
||||
hasRegistrationFee: boolean;
|
||||
registrationFee: number;
|
||||
maxParticipants?: number | null;
|
||||
requirements?: string[];
|
||||
refreshment_expense?: number;
|
||||
transportation_expense?: number;
|
||||
session_coverage_fee?: number;
|
||||
total_expense?: number;
|
||||
paymentLink?: string;
|
||||
isClubEvent?: boolean;
|
||||
eventScope?: string;
|
||||
}
|
||||
|
||||
export const EventHistory: React.FC = () => {
|
||||
@@ -294,38 +309,129 @@ export const EventHistory: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-2 gap-8">
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-text-muted">
|
||||
<Calendar className="w-3.5 h-3.5" />
|
||||
Date & Time
|
||||
</div>
|
||||
<p className="text-sm font-bold text-text-dark">{new Date(selectedEvent.startDate).toLocaleString()}</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-text-muted">
|
||||
<MapPin className="w-3.5 h-3.5" />
|
||||
Location
|
||||
</div>
|
||||
<p className="text-sm font-bold text-text-dark">{selectedEvent.location}</p>
|
||||
{/* Event Classification */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="p-4 bg-slate-50 rounded-2xl space-y-1">
|
||||
<div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-text-muted">
|
||||
<FileText className="w-3.5 h-3.5" />
|
||||
Event Type
|
||||
</div>
|
||||
<p className="text-sm font-bold text-text-dark">{selectedEvent.type || 'N/A'}</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-text-muted">
|
||||
<Building2 className="w-3.5 h-3.5" />
|
||||
Department
|
||||
</div>
|
||||
<p className="text-sm font-bold text-text-dark">{selectedEvent.department}</p>
|
||||
<div className="p-4 bg-slate-50 rounded-2xl space-y-1">
|
||||
<div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-text-muted">
|
||||
<Layers className="w-3.5 h-3.5" />
|
||||
Hub Category
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-text-muted">
|
||||
<Users className="w-3.5 h-3.5" />
|
||||
Target Batches
|
||||
</div>
|
||||
<p className="text-sm font-bold text-text-dark">{selectedEvent.academicYears?.join(', ') || 'N/A'}</p>
|
||||
<p className="text-sm font-bold text-text-dark capitalize">
|
||||
{selectedEvent.category === 'TECHNICAL' ? 'Technical'
|
||||
: selectedEvent.category === 'NON-TECHNICAL' ? 'Non-Technical'
|
||||
: selectedEvent.category === 'WORKSHOP' ? 'Workshop'
|
||||
: selectedEvent.category === 'CENTRE-ACTIVITY' ? 'Centre Based Activity'
|
||||
: selectedEvent.category || 'N/A'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Dates */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="p-4 bg-slate-50 rounded-2xl space-y-1">
|
||||
<div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-text-muted">
|
||||
<Calendar className="w-3.5 h-3.5" />
|
||||
Start Date & Time
|
||||
</div>
|
||||
<p className="text-sm font-bold text-text-dark">
|
||||
{selectedEvent.startDate
|
||||
? new Date(selectedEvent.startDate).toLocaleString('en-IN', { timeZone: 'Asia/Kolkata', day: '2-digit', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit', hour12: false }) + ' IST'
|
||||
: 'N/A'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-4 bg-slate-50 rounded-2xl space-y-1">
|
||||
<div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-text-muted">
|
||||
<Calendar className="w-3.5 h-3.5" />
|
||||
End Date & Time
|
||||
</div>
|
||||
<p className="text-sm font-bold text-text-dark">
|
||||
{selectedEvent.endDate
|
||||
? new Date(selectedEvent.endDate).toLocaleString('en-IN', { timeZone: 'Asia/Kolkata', day: '2-digit', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit', hour12: false }) + ' IST'
|
||||
: 'N/A'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Location & Capacity */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="p-4 bg-slate-50 rounded-2xl space-y-1">
|
||||
<div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-text-muted">
|
||||
<MapPin className="w-3.5 h-3.5" />
|
||||
Location
|
||||
</div>
|
||||
<p className="text-sm font-bold text-text-dark">{selectedEvent.location || 'N/A'}</p>
|
||||
</div>
|
||||
<div className="p-4 bg-slate-50 rounded-2xl space-y-1">
|
||||
<div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-text-muted">
|
||||
<Users className="w-3.5 h-3.5" />
|
||||
Total Capacity
|
||||
</div>
|
||||
<p className="text-sm font-bold text-text-dark">
|
||||
{selectedEvent.maxParticipants ? selectedEvent.maxParticipants.toLocaleString() : 'Unlimited'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Scope */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="p-4 bg-slate-50 rounded-2xl space-y-1">
|
||||
<div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-text-muted">
|
||||
<Building2 className="w-3.5 h-3.5" />
|
||||
Event Scope
|
||||
</div>
|
||||
<p className="text-sm font-bold text-text-dark">
|
||||
{selectedEvent.department === 'Institutional' ? 'Institutional Event' : 'Departmental Event'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-4 bg-slate-50 rounded-2xl space-y-1">
|
||||
<div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-text-muted">
|
||||
<Building2 className="w-3.5 h-3.5" />
|
||||
Proposing Department
|
||||
</div>
|
||||
<p className="text-sm font-bold text-text-dark">
|
||||
{(selectedEvent.proposer?.department && selectedEvent.proposer?.department !== 'N/A')
|
||||
? selectedEvent.proposer.department
|
||||
: (selectedEvent.proposer?.role === 'ADMIN' ? 'System Administrator' : selectedEvent.department)
|
||||
|| 'N/A'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Target Departments & Batches */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-text-muted">
|
||||
<Users className="w-3.5 h-3.5" />
|
||||
Target Audience
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<div className="p-3 bg-slate-50 rounded-xl">
|
||||
<p className="text-[9px] font-black uppercase tracking-widest text-text-muted mb-1">Departments</p>
|
||||
<p className="text-xs font-bold text-text-dark">
|
||||
{(selectedEvent.targetDepartments && selectedEvent.targetDepartments.length > 0)
|
||||
? selectedEvent.targetDepartments.join(', ')
|
||||
: selectedEvent.department || 'N/A'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-3 bg-slate-50 rounded-xl">
|
||||
<p className="text-[9px] font-black uppercase tracking-widest text-text-muted mb-1">Batches</p>
|
||||
<p className="text-xs font-bold text-text-dark">
|
||||
{selectedEvent.academicYears?.length > 0 ? selectedEvent.academicYears.join(', ') : 'All'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-3 bg-slate-50 rounded-xl">
|
||||
<p className="text-[9px] font-black uppercase tracking-widest text-text-muted mb-1">Sections</p>
|
||||
<p className="text-xs font-bold text-text-dark">
|
||||
{(selectedEvent.targetedSections && selectedEvent.targetedSections.length > 0)
|
||||
? selectedEvent.targetedSections.join(', ')
|
||||
: 'All Sections'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -347,16 +453,77 @@ export const EventHistory: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-2 gap-8 pt-4 border-t border-slate-50">
|
||||
<div className="p-4 bg-slate-50 rounded-2xl">
|
||||
<div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-text-muted mb-1">
|
||||
<Wallet className="w-3.5 h-3.5" />
|
||||
Budget
|
||||
{/* Event Amenities / Requirements */}
|
||||
{selectedEvent.requirements && selectedEvent.requirements.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-text-muted">
|
||||
<CheckCircle2 className="w-3.5 h-3.5 text-emerald-500" />
|
||||
Event Amenities
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{selectedEvent.requirements.map((req, idx) => (
|
||||
<span key={idx} className="px-3 py-1 bg-emerald-50 text-emerald-700 rounded-lg text-[10px] font-black uppercase tracking-widest border border-emerald-100">
|
||||
{req}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-lg font-black text-text-dark">₹{selectedEvent.budget?.toLocaleString() || '0'}</p>
|
||||
</div>
|
||||
<div className="p-4 bg-slate-50 rounded-2xl">
|
||||
<div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-text-muted mb-1">
|
||||
)}
|
||||
|
||||
{/* Financial Details */}
|
||||
<div className="space-y-3 border-t border-slate-50 pt-4">
|
||||
<div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-text-muted">
|
||||
<Wallet className="w-3.5 h-3.5" />
|
||||
Financial Details
|
||||
</div>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||||
<div className="p-3 bg-slate-50 rounded-xl">
|
||||
<p className="text-[9px] font-black uppercase tracking-widest text-text-muted mb-1">Budget</p>
|
||||
<p className="text-sm font-black text-text-dark">₹{selectedEvent.budget?.toLocaleString() || '0'}</p>
|
||||
</div>
|
||||
{(selectedEvent.refreshment_expense || 0) > 0 && (
|
||||
<div className="p-3 bg-slate-50 rounded-xl">
|
||||
<p className="text-[9px] font-black uppercase tracking-widest text-text-muted mb-1">Refreshments</p>
|
||||
<p className="text-sm font-black text-text-dark">₹{selectedEvent.refreshment_expense?.toLocaleString()}</p>
|
||||
</div>
|
||||
)}
|
||||
{(selectedEvent.transportation_expense || 0) > 0 && (
|
||||
<div className="p-3 bg-slate-50 rounded-xl">
|
||||
<p className="text-[9px] font-black uppercase tracking-widest text-text-muted mb-1">Transportation</p>
|
||||
<p className="text-sm font-black text-text-dark">₹{selectedEvent.transportation_expense?.toLocaleString()}</p>
|
||||
</div>
|
||||
)}
|
||||
{(selectedEvent.session_coverage_fee || 0) > 0 && (
|
||||
<div className="p-3 bg-slate-50 rounded-xl">
|
||||
<p className="text-[9px] font-black uppercase tracking-widest text-text-muted mb-1">Session Coverage</p>
|
||||
<p className="text-sm font-black text-text-dark">₹{selectedEvent.session_coverage_fee?.toLocaleString()}</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="p-3 bg-brand-navy rounded-xl">
|
||||
<p className="text-[9px] font-black uppercase tracking-widest text-white/60 mb-1">Total Expense</p>
|
||||
<p className="text-sm font-black text-white">₹{(selectedEvent.total_expense || selectedEvent.budget || 0).toLocaleString()}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Registration / Free Event */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="p-4 bg-slate-50 rounded-2xl space-y-1">
|
||||
<div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-text-muted">
|
||||
<Ticket className="w-3.5 h-3.5" />
|
||||
Registration
|
||||
</div>
|
||||
<p className="text-sm font-bold text-text-dark">
|
||||
{selectedEvent.hasRegistrationFee
|
||||
? `Paid — ₹${selectedEvent.registrationFee?.toLocaleString()}`
|
||||
: 'Free Event'}
|
||||
</p>
|
||||
{selectedEvent.hasRegistrationFee && selectedEvent.paymentLink && (
|
||||
<a href={selectedEvent.paymentLink} target="_blank" rel="noopener noreferrer" className="text-[10px] text-brand-indigo font-bold underline">Payment Link ↗</a>
|
||||
)}
|
||||
</div>
|
||||
<div className="p-4 bg-slate-50 rounded-2xl space-y-1">
|
||||
<div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-text-muted">
|
||||
<Ticket className="w-3.5 h-3.5" />
|
||||
Status
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import React, { useState, useEffect, useRef, useMemo } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import {
|
||||
Calendar,
|
||||
MapPin,
|
||||
@@ -25,6 +27,7 @@ import {
|
||||
AlertTriangle,
|
||||
FileSpreadsheet
|
||||
} from 'lucide-react';
|
||||
/* eslint-enable @typescript-eslint/no-unused-vars */
|
||||
import { useAuth } from '../../context/AuthContext';
|
||||
import { cn } from '../../lib/utils';
|
||||
import { API_BASE_URL } from '../../lib/config';
|
||||
@@ -116,7 +119,7 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
|
||||
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const [venues, setVenues] = useState<{name: string, capacity: number | null}[]>([
|
||||
const [venues, setVenues] = useState<{ name: string, capacity: number | null }[]>([
|
||||
{ name: 'N/A', capacity: null },
|
||||
{ name: 'Others.', capacity: null }
|
||||
]);
|
||||
@@ -175,8 +178,21 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
|
||||
}
|
||||
};
|
||||
|
||||
const getInitialScope = () => {
|
||||
if (initialData?.category) {
|
||||
if (['TECHNICAL', 'NON-TECHNICAL', 'WORKSHOP', 'ACADEMIC'].includes(initialData.category)) {
|
||||
return 'DEPARTMENT';
|
||||
}
|
||||
if (initialData.category === 'CENTRE-ACTIVITY') {
|
||||
return 'CENTRE';
|
||||
}
|
||||
return initialData.category as any;
|
||||
}
|
||||
return initialData?.isClubEvent ? 'INSTITUTIONAL' : (isPlacementCell ? 'PLACEMENT' : 'DEPARTMENT');
|
||||
};
|
||||
|
||||
const [eventScope, setEventScope] = useState<'INSTITUTIONAL' | 'DEPARTMENT' | 'CLUB' | 'PLACEMENT' | 'CENTRE'>(
|
||||
initialData?.isClubEvent ? 'INSTITUTIONAL' : (isPlacementCell ? 'PLACEMENT' : 'DEPARTMENT')
|
||||
getInitialScope()
|
||||
);
|
||||
const [conflictData, setConflictData] = useState<{ message: string, conflicts: any[], canOverride: boolean } | null>(null);
|
||||
|
||||
@@ -191,6 +207,7 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
|
||||
);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const parseDateForInput = (dateStr?: string) => {
|
||||
if (!dateStr) return '';
|
||||
try {
|
||||
@@ -225,8 +242,8 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
|
||||
eventName: initialData?.eventName || '',
|
||||
startDate: initialData?.startDate ? formatDateTimeForInput(initialData.startDate) : '',
|
||||
endDate: initialData?.endDate ? formatDateTimeForInput(initialData.endDate) : '',
|
||||
eventType: isClubVariant ? 'CLUB' : (initialData?.eventType || 'Guest Lecture'),
|
||||
category: isClubVariant ? 'CLUB' : (initialData?.category || 'ACADEMIC'),
|
||||
eventType: initialData?.eventType || (isClubVariant ? 'CLUB' : 'Guest Lecture'),
|
||||
category: initialData?.category || (isClubVariant ? 'CLUB' : 'TECHNICAL'),
|
||||
institution: initialData?.institution || 'RIT',
|
||||
department: initialData?.department || (isFaculty ? (user?.department || 'CSE') : 'CSE'),
|
||||
academicYears: initialData?.academicYears || [] as string[],
|
||||
@@ -262,8 +279,8 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
|
||||
// Sync financial totals
|
||||
useEffect(() => {
|
||||
const total = (parseFloat(formData.refreshment_expense) || 0) +
|
||||
(parseFloat(formData.transportation_expense) || 0) +
|
||||
(parseFloat(formData.session_coverage_fee) || 0);
|
||||
(parseFloat(formData.transportation_expense) || 0) +
|
||||
(parseFloat(formData.session_coverage_fee) || 0);
|
||||
setFormData(prev => ({ ...prev, total_expense: total.toFixed(2) }));
|
||||
}, [formData.refreshment_expense, formData.transportation_expense, formData.session_coverage_fee]);
|
||||
|
||||
@@ -281,15 +298,30 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
|
||||
customVenue: '',
|
||||
});
|
||||
|
||||
const prevEventScope = useRef(eventScope);
|
||||
const hasInitializedEdit = useRef(!!initialData?.isEditMode);
|
||||
|
||||
useEffect(() => {
|
||||
const scopeChanged = prevEventScope.current !== eventScope;
|
||||
prevEventScope.current = eventScope;
|
||||
|
||||
// Skip reset entirely on initial mount when editing an existing event
|
||||
if (hasInitializedEdit.current) {
|
||||
hasInitializedEdit.current = false;
|
||||
if (!scopeChanged) return; // user?.department changed, not scope — don't reset
|
||||
}
|
||||
|
||||
// Only reset when the scope actually changed (user clicked a different tab)
|
||||
if (!scopeChanged) return;
|
||||
|
||||
// Reset event name and update default event type on scope change
|
||||
const availableCentres = CENTRE_MAPPING[user?.department || ''] || CENTRE_MAPPING['Placement Department'] || [];
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
eventName: '',
|
||||
eventType: eventScope === 'INSTITUTIONAL' ? 'Institutional' :
|
||||
eventScope === 'CLUB' ? 'Club' :
|
||||
eventScope === 'PLACEMENT' ? 'Placement' : '',
|
||||
eventType: eventScope === 'INSTITUTIONAL' ? '' :
|
||||
eventScope === 'CLUB' ? 'Club' :
|
||||
eventScope === 'PLACEMENT' ? 'Placement' : '',
|
||||
category: eventScope === 'CENTRE' ? 'CENTRE' : (eventScope === 'CLUB' ? 'CLUB' : 'ACADEMIC'),
|
||||
centreName: eventScope === 'CENTRE' ? (availableCentres[0] || '') : prev.centreName
|
||||
}));
|
||||
@@ -297,12 +329,7 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
|
||||
|
||||
const [batches, setBatches] = useState<any[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchClasses();
|
||||
fetchExistingEvents();
|
||||
fetchClassStrengths();
|
||||
fetchBatches();
|
||||
}, []);
|
||||
// data-fetch effect is declared after the fetch functions below
|
||||
|
||||
const fetchBatches = async () => {
|
||||
try {
|
||||
@@ -316,6 +343,7 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
|
||||
}
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const deptBatches = useMemo(() => {
|
||||
const dept = eventScope === 'DEPARTMENT' ? formData.department : (user?.department || 'CSE');
|
||||
return batches.filter(b => b.department === dept);
|
||||
@@ -378,10 +406,19 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
|
||||
}
|
||||
};
|
||||
|
||||
/* eslint-disable react-hooks/set-state-in-effect */
|
||||
useEffect(() => {
|
||||
fetchClasses();
|
||||
fetchExistingEvents();
|
||||
fetchClassStrengths();
|
||||
fetchBatches();
|
||||
}, []);
|
||||
/* eslint-enable react-hooks/set-state-in-effect */
|
||||
|
||||
const availableYears = Array.from(new Set(
|
||||
classes
|
||||
.filter(c => c.institution.trim() === formData.institution.trim() &&
|
||||
(eventScope === 'INSTITUTIONAL' || eventScope === 'CLUB' || eventScope === 'PLACEMENT' || (eventScope === 'DEPARTMENT' && isPlacementCell) ? true : c.department.trim() === formData.department.trim()))
|
||||
(eventScope === 'INSTITUTIONAL' || eventScope === 'CLUB' || eventScope === 'PLACEMENT' || (eventScope === 'DEPARTMENT' && isPlacementCell) ? true : c.department.trim() === formData.department.trim()))
|
||||
.map(c => c.academicYear)
|
||||
)).sort();
|
||||
|
||||
@@ -425,9 +462,9 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
|
||||
const status = e.status?.toUpperCase();
|
||||
const isActive = status === 'APPROVED' || status === 'COMPLETED' || status === 'PENDING_PR' || status === 'REQUESTED';
|
||||
return e.type?.toLowerCase() === formData.eventType?.toLowerCase() &&
|
||||
isActive &&
|
||||
e.targetedSections?.includes(section) &&
|
||||
formData.academicYears.some(y => e.academicYears?.includes(y));
|
||||
isActive &&
|
||||
e.targetedSections?.includes(section) &&
|
||||
formData.academicYears.some(y => e.academicYears?.includes(y));
|
||||
});
|
||||
};
|
||||
|
||||
@@ -461,32 +498,41 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
|
||||
}, [formData.targetDepartments, formData.department, formData.academicYears, formData.targetedSections, formData.allBatches, formData.allSections, formData.institution, classStrengths, eventScope, isPlacementCell, availableYears, classes]);
|
||||
|
||||
const filteredVenues = React.useMemo(() => {
|
||||
if (totalExpectedStrength === 0) return venues;
|
||||
let list = venues;
|
||||
if (totalExpectedStrength > 0) {
|
||||
// Find the smallest capacity venue that can accommodate the totalExpectedStrength
|
||||
const validCapacities = venues
|
||||
.map(v => v.capacity)
|
||||
.filter((cap): cap is number => cap !== null && cap >= totalExpectedStrength);
|
||||
|
||||
// Find the smallest capacity venue that can accommodate the totalExpectedStrength
|
||||
const validCapacities = venues
|
||||
.map(v => v.capacity)
|
||||
.filter((cap): cap is number => cap !== null && cap >= totalExpectedStrength);
|
||||
const minSufficientCapacity = validCapacities.length > 0 ? Math.min(...validCapacities) : totalExpectedStrength;
|
||||
|
||||
const minSufficientCapacity = validCapacities.length > 0 ? Math.min(...validCapacities) : totalExpectedStrength;
|
||||
list = venues.filter(v => {
|
||||
if (v.name === 'N/A' || v.name === 'Others.' || v.capacity === null) return true;
|
||||
if (initialData?.venue && v.name === initialData.venue) return true;
|
||||
|
||||
return venues.filter(v => {
|
||||
if (v.name === 'N/A' || v.name === 'Others.' || v.capacity === null) return true;
|
||||
|
||||
// Capacity must be at least the audience size, and not exceed the max of (1.5x audience OR the smallest sufficient capacity)
|
||||
const maxAllowedCapacity = Math.max(totalExpectedStrength * 1.5, minSufficientCapacity);
|
||||
return v.capacity >= totalExpectedStrength && v.capacity <= maxAllowedCapacity;
|
||||
});
|
||||
}, [venues, totalExpectedStrength]);
|
||||
// Capacity must be at least the audience size, and not exceed the max of (1.5x audience OR the smallest sufficient capacity)
|
||||
const maxAllowedCapacity = Math.max(totalExpectedStrength * 1.5, minSufficientCapacity);
|
||||
return v.capacity >= totalExpectedStrength && v.capacity <= maxAllowedCapacity;
|
||||
});
|
||||
}
|
||||
if (initialData?.venue && !list.some(v => v.name === initialData.venue)) {
|
||||
list = [...list, { name: initialData.venue, capacity: null }];
|
||||
}
|
||||
return list;
|
||||
}, [venues, totalExpectedStrength, initialData?.venue]);
|
||||
|
||||
useEffect(() => {
|
||||
if (formData.venue && formData.venue !== 'N/A' && formData.venue !== 'Others.') {
|
||||
const isStillAvailable = filteredVenues.some(v => v.name === formData.venue);
|
||||
if (!isStillAvailable) {
|
||||
if (initialData?.isEditMode && formData.venue === initialData.venue) {
|
||||
return;
|
||||
}
|
||||
setFormData(prev => ({ ...prev, venue: 'N/A' }));
|
||||
}
|
||||
}
|
||||
}, [filteredVenues]);
|
||||
}, [filteredVenues, formData.venue, initialData]);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
@@ -517,12 +563,6 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
|
||||
|
||||
const groupRequestId = crypto.randomUUID();
|
||||
const batchesToAssign = formData.allBatches ? availableYears : formData.academicYears;
|
||||
let deptsToCreate = (eventScope === 'CLUB' || eventScope === 'PLACEMENT' || (eventScope === 'DEPARTMENT' && isPlacementCell)) ? formData.targetDepartments : [formData.department];
|
||||
|
||||
// For institutional events, create only ONE global event record
|
||||
if (eventScope === 'INSTITUTIONAL') {
|
||||
deptsToCreate = ['Institutional'];
|
||||
}
|
||||
|
||||
if (eventScope === 'CENTRE' && !formData.centreName) {
|
||||
setError('Please select a centre.');
|
||||
@@ -530,7 +570,7 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
|
||||
return;
|
||||
}
|
||||
|
||||
if (deptsToCreate.length === 0) {
|
||||
if (formData.targetDepartments.length === 0 && (eventScope === 'INSTITUTIONAL' || eventScope === 'CLUB' || eventScope === 'PLACEMENT' || (eventScope === 'DEPARTMENT' && isPlacementCell))) {
|
||||
setError(`Please select at least one target department for this ${eventScope.toLowerCase()} event.`);
|
||||
setIsSubmitting(false);
|
||||
return;
|
||||
@@ -572,23 +612,62 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
|
||||
|
||||
if (!res.ok) {
|
||||
const err = await res.json();
|
||||
throw new Error(err.message || 'Update failed');
|
||||
throw new Error(err.message || 'Update failed');
|
||||
}
|
||||
} else {
|
||||
for (const dept of deptsToCreate) {
|
||||
// Filter sections that belong to THIS department
|
||||
const deptSections = (eventScope === 'DEPARTMENT' || eventScope === 'PLACEMENT')
|
||||
? formData.targetedSections.filter(s =>
|
||||
classes.some(c => c.department.trim() === dept.trim() && c.sections.includes(s))
|
||||
)
|
||||
: [];
|
||||
const primaryPayload = {
|
||||
...formData,
|
||||
startDate: earliestStart.toISOString(),
|
||||
endDate: latestEnd.toISOString(),
|
||||
deptLimits,
|
||||
deptSectionLimits,
|
||||
maxParticipants: formData.maxParticipants === '' ? null : Number(formData.maxParticipants),
|
||||
durationDays: 1,
|
||||
dayConfigs: [],
|
||||
refreshment_expense: Number(formData.refreshment_expense) || 0,
|
||||
transportation_expense: Number(formData.transportation_expense) || 0,
|
||||
session_coverage_fee: Number(formData.session_coverage_fee) || 0,
|
||||
total_expense: Number(formData.total_expense) || 0,
|
||||
department: formData.department,
|
||||
venue: formData.venue === 'Others.' ? formData.customVenue : formData.venue,
|
||||
groupRequestId,
|
||||
academicYears: batchesToAssign,
|
||||
targetedSections: formData.targetedSections,
|
||||
userId: user?.id,
|
||||
budget: formData.budget === '' ? 0 : Number(formData.budget),
|
||||
registrationFee: formData.hasRegistrationFee ? Number(formData.registrationFee) : 0,
|
||||
category: eventScope === 'INSTITUTIONAL' ? 'INSTITUTIONAL' :
|
||||
eventScope === 'CLUB' ? 'CLUB' :
|
||||
eventScope === 'CENTRE' ? 'CENTRE-ACTIVITY' :
|
||||
eventScope === 'PLACEMENT' ? 'PLACEMENT' :
|
||||
formData.category,
|
||||
eventType: formData.eventType,
|
||||
centreName: eventScope === 'CENTRE' ? formData.centreName : undefined,
|
||||
isPublicEvent: eventScope === 'CENTRE' ? formData.isPublicEvent : undefined,
|
||||
image: formData.image
|
||||
};
|
||||
|
||||
// If it's a placement event and we're selective, only create for depts with selected sections
|
||||
if (eventScope === 'PLACEMENT' && formData.targetedSections.length > 0 && deptSections.length === 0) {
|
||||
continue;
|
||||
const res = await fetch(API_BASE_URL + '/api/events/propose', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
...primaryPayload,
|
||||
cancelConflicting: false
|
||||
}),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const err = await res.json();
|
||||
if (res.status === 409 && err.canOverride) {
|
||||
setConflictData(err);
|
||||
setIsSubmitting(false);
|
||||
return;
|
||||
}
|
||||
throw new Error(err.message || 'Submission failed');
|
||||
}
|
||||
|
||||
const primaryPayload = {
|
||||
if (!formData.allSections && showAlternateEvent && (eventScope === 'DEPARTMENT' || eventScope === 'PLACEMENT') && leftOutSections.length > 0) {
|
||||
const altPayload = {
|
||||
...formData,
|
||||
startDate: earliestStart.toISOString(),
|
||||
endDate: latestEnd.toISOString(),
|
||||
@@ -601,78 +680,23 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
|
||||
transportation_expense: Number(formData.transportation_expense) || 0,
|
||||
session_coverage_fee: Number(formData.session_coverage_fee) || 0,
|
||||
total_expense: Number(formData.total_expense) || 0,
|
||||
department: dept,
|
||||
venue: formData.venue === 'Others.' ? formData.customVenue : formData.venue,
|
||||
department: formData.department,
|
||||
groupRequestId,
|
||||
eventName: alternateEvent.eventName || `${formData.eventName} (Division 2)`,
|
||||
venue: alternateEvent.venue === 'Others.' ? alternateEvent.customVenue : alternateEvent.venue,
|
||||
targetedSections: leftOutSections,
|
||||
academicYears: batchesToAssign,
|
||||
targetedSections: deptSections,
|
||||
userId: user?.id,
|
||||
budget: formData.budget === '' ? 0 : Number(formData.budget),
|
||||
budget: 0,
|
||||
registrationFee: formData.hasRegistrationFee ? Number(formData.registrationFee) : 0,
|
||||
category: eventScope === 'CLUB' ? 'CLUB' : (eventScope === 'INSTITUTIONAL' ? 'INSTITUTIONAL' : (eventScope === 'CENTRE' ? 'CENTRE' : 'ACADEMIC')),
|
||||
eventType: formData.eventType,
|
||||
centreName: eventScope === 'CENTRE' ? formData.centreName : undefined,
|
||||
isPublicEvent: eventScope === 'CENTRE' ? formData.isPublicEvent : undefined,
|
||||
image: formData.image
|
||||
eventType: formData.eventType
|
||||
};
|
||||
|
||||
const res = await fetch(API_BASE_URL + '/api/events/propose', {
|
||||
await fetch(API_BASE_URL + '/api/events/propose', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
...primaryPayload,
|
||||
cancelConflicting: false
|
||||
}),
|
||||
body: JSON.stringify(altPayload),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const err = await res.json();
|
||||
if (res.status === 409 && err.canOverride) {
|
||||
setConflictData(err);
|
||||
setIsSubmitting(false);
|
||||
return;
|
||||
}
|
||||
throw new Error(`[${dept}] ${err.message || 'Submission failed'}`);
|
||||
}
|
||||
|
||||
if (!formData.allSections && showAlternateEvent && (eventScope === 'DEPARTMENT' || eventScope === 'PLACEMENT') && leftOutSections.length > 0) {
|
||||
const deptLeftOut = leftOutSections.filter(s =>
|
||||
classes.some(c => c.department.trim() === dept.trim() && c.sections.includes(s))
|
||||
);
|
||||
|
||||
if (deptLeftOut.length > 0) {
|
||||
const altPayload = {
|
||||
...formData,
|
||||
startDate: earliestStart.toISOString(),
|
||||
endDate: latestEnd.toISOString(),
|
||||
deptLimits,
|
||||
deptSectionLimits,
|
||||
maxParticipants: formData.maxParticipants === '' ? null : Number(formData.maxParticipants),
|
||||
durationDays: 1,
|
||||
dayConfigs: [],
|
||||
refreshment_expense: Number(formData.refreshment_expense) || 0,
|
||||
transportation_expense: Number(formData.transportation_expense) || 0,
|
||||
session_coverage_fee: Number(formData.session_coverage_fee) || 0,
|
||||
total_expense: Number(formData.total_expense) || 0,
|
||||
department: dept,
|
||||
groupRequestId,
|
||||
eventName: alternateEvent.eventName || `${formData.eventName} (Division 2)`,
|
||||
venue: alternateEvent.venue === 'Others.' ? alternateEvent.customVenue : alternateEvent.venue,
|
||||
targetedSections: deptLeftOut,
|
||||
academicYears: batchesToAssign,
|
||||
userId: user?.id,
|
||||
budget: 0,
|
||||
registrationFee: formData.hasRegistrationFee ? Number(formData.registrationFee) : 0,
|
||||
eventType: formData.eventType
|
||||
};
|
||||
|
||||
await fetch(API_BASE_URL + '/api/events/propose', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(altPayload),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
setSubmitted(true);
|
||||
@@ -793,9 +817,9 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
|
||||
<div>
|
||||
<h2 className="text-3xl font-black tracking-tight mb-1">
|
||||
{eventScope === 'INSTITUTIONAL' ? 'Institutional Proposal' :
|
||||
eventScope === 'CLUB' ? 'Club Proposal' :
|
||||
eventScope === 'CENTRE' ? 'Centre Event Proposal' :
|
||||
eventScope === 'PLACEMENT' ? 'Placement Coordinator Schedule' : 'Departmental Proposal'}
|
||||
eventScope === 'CLUB' ? 'Club Proposal' :
|
||||
eventScope === 'CENTRE' ? 'Centre Event Proposal' :
|
||||
eventScope === 'PLACEMENT' ? 'Placement Coordinator Schedule' : 'Departmental Proposal'}
|
||||
</h2>
|
||||
<p className="text-white/60 font-medium text-sm">
|
||||
{isFaculty ? `Faculty of ${user?.department}` : 'Administrative Portal'}
|
||||
@@ -814,7 +838,7 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
|
||||
<select
|
||||
required
|
||||
value={formData.eventType}
|
||||
onChange={e => setFormData({...formData, eventType: e.target.value})}
|
||||
onChange={e => setFormData({ ...formData, eventType: e.target.value })}
|
||||
className="w-full bg-slate-50 border border-transparent rounded-2xl py-4 px-6 text-sm font-semibold appearance-none cursor-pointer group-hover:bg-slate-100 transition-all pr-12"
|
||||
>
|
||||
<option value="">Select Type</option>
|
||||
@@ -830,50 +854,80 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
|
||||
<input
|
||||
type="text" required
|
||||
value={formData.eventName}
|
||||
onChange={e => setFormData({...formData, eventName: e.target.value})}
|
||||
onChange={e => setFormData({ ...formData, eventName: e.target.value })}
|
||||
className="w-full bg-slate-50 border border-transparent rounded-2xl py-4 px-6 text-sm font-semibold focus:bg-white focus:border-brand-indigo/30 transition-all"
|
||||
placeholder="e.g. Workshop on Generative AI"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : eventScope === 'INSTITUTIONAL' ? (
|
||||
<div className="md:col-span-2 relative group">
|
||||
<label className="text-[10px] font-black uppercase tracking-widest text-text-muted mb-3 block">Select Institutional Event</label>
|
||||
<div className="relative">
|
||||
<select
|
||||
required
|
||||
value={formData.eventName}
|
||||
onChange={e => setFormData({...formData, eventName: e.target.value, eventType: 'Institutional'})}
|
||||
className="w-full bg-slate-50 border border-transparent rounded-2xl py-4 px-6 text-sm font-semibold appearance-none cursor-pointer group-hover:bg-slate-100 transition-all pr-12"
|
||||
>
|
||||
<option value="">Choose Institutional Event...</option>
|
||||
{availableInstitutionalEvents.map(ie => (
|
||||
<option key={ie.id} value={ie.name}>{ie.name}</option>
|
||||
))}
|
||||
</select>
|
||||
<ChevronDown className="absolute right-6 top-1/2 -translate-y-1/2 w-4 h-4 text-text-muted pointer-events-none" />
|
||||
<>
|
||||
<div>
|
||||
<label className="text-[10px] font-black uppercase tracking-widest text-text-muted mb-3 block">Select Institutional Event</label>
|
||||
<div className="relative group">
|
||||
<select
|
||||
required
|
||||
value={formData.eventType}
|
||||
onChange={e => setFormData({ ...formData, eventType: e.target.value })}
|
||||
className="w-full bg-slate-50 border border-transparent rounded-2xl py-4 px-6 text-sm font-semibold appearance-none cursor-pointer group-hover:bg-slate-100 transition-all pr-12"
|
||||
>
|
||||
<option value="">Choose Institutional Event...</option>
|
||||
{availableInstitutionalEvents.map(ie => (
|
||||
<option key={ie.id} value={ie.name}>{ie.name}</option>
|
||||
))}
|
||||
</select>
|
||||
<ChevronDown className="absolute right-6 top-1/2 -translate-y-1/2 w-4 h-4 text-text-muted pointer-events-none" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-[10px] font-black uppercase tracking-widest text-text-muted mb-3 block">Event Name</label>
|
||||
<input
|
||||
type="text" required
|
||||
value={formData.eventName}
|
||||
onChange={e => setFormData({ ...formData, eventName: e.target.value })}
|
||||
className="w-full bg-slate-50 border border-transparent rounded-2xl py-4 px-6 text-sm font-semibold focus:bg-white focus:border-brand-indigo/30 transition-all"
|
||||
placeholder="e.g. Game Jam 2026"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="md:col-span-2">
|
||||
<label className="text-[10px] font-black uppercase tracking-widest text-text-muted mb-3 block">Event Name</label>
|
||||
<input
|
||||
type="text" required
|
||||
value={formData.eventName}
|
||||
onChange={e => setFormData({...formData, eventName: e.target.value, eventType: eventScope === 'CLUB' ? 'Club' : 'Placement'})}
|
||||
onChange={e => setFormData({ ...formData, eventName: e.target.value, eventType: eventScope === 'CLUB' ? 'Club' : 'Placement' })}
|
||||
className="w-full bg-slate-50 border border-transparent rounded-2xl py-4 px-6 text-sm font-semibold focus:bg-white focus:border-brand-indigo/30 transition-all"
|
||||
placeholder={eventScope === 'PLACEMENT' ? "Enter placement activity name..." : "Enter event title..."}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="md:col-span-2">
|
||||
<label className="text-[10px] font-black uppercase tracking-widest text-text-muted mb-3 block">Hub Category</label>
|
||||
<div className="relative group">
|
||||
<select
|
||||
required
|
||||
value={formData.category}
|
||||
onChange={e => setFormData({ ...formData, category: e.target.value })}
|
||||
className="w-full bg-slate-50 border border-transparent rounded-2xl py-4 px-6 text-sm font-semibold appearance-none cursor-pointer group-hover:bg-slate-100 transition-all pr-12"
|
||||
>
|
||||
<option value="TECHNICAL">Technical Event</option>
|
||||
<option value="NON-TECHNICAL">Non-Technical Event</option>
|
||||
<option value="WORKSHOP">Workshop</option>
|
||||
<option value="CENTRE-ACTIVITY">Centre Based Activity</option>
|
||||
</select>
|
||||
<ChevronDown className="absolute right-6 top-1/2 -translate-y-1/2 w-4 h-4 text-text-muted pointer-events-none" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{initialData?.isEditMode && user?.role === 'ADMIN' && (
|
||||
<div className="md:col-span-2">
|
||||
<label className="text-[10px] font-black uppercase tracking-widest text-text-muted mb-3 block">Event Status (Admin Only)</label>
|
||||
<div className="relative group">
|
||||
<select
|
||||
value={formData.status}
|
||||
onChange={e => setFormData({...formData, status: e.target.value})}
|
||||
onChange={e => setFormData({ ...formData, status: e.target.value })}
|
||||
className="w-full bg-brand-glow border border-brand-indigo/20 rounded-2xl py-4 px-6 text-sm font-bold text-brand-indigo appearance-none cursor-pointer group-hover:bg-brand-glow/80 transition-all pr-12"
|
||||
>
|
||||
<option value="REQUESTED">REQUESTED</option>
|
||||
@@ -895,7 +949,7 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
|
||||
<FileText className="absolute left-5 top-5 w-5 h-5 text-text-muted" />
|
||||
<textarea
|
||||
value={formData.description}
|
||||
onChange={e => setFormData({...formData, description: e.target.value})}
|
||||
onChange={e => setFormData({ ...formData, description: e.target.value })}
|
||||
rows={4}
|
||||
className="w-full bg-slate-50 border border-transparent rounded-2xl py-5 pl-14 pr-6 text-sm font-semibold focus:bg-white focus:border-brand-indigo/30 transition-all resize-none"
|
||||
placeholder="Provide brief details about the event objective and highlights..."
|
||||
@@ -909,7 +963,7 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
|
||||
</label>
|
||||
<select
|
||||
value={formData.venue}
|
||||
onChange={e => setFormData({...formData, venue: e.target.value})}
|
||||
onChange={e => setFormData({ ...formData, venue: e.target.value })}
|
||||
className="w-full bg-slate-50 border border-transparent rounded-2xl py-4 px-6 text-sm font-semibold appearance-none cursor-pointer group-hover:bg-slate-100 transition-all"
|
||||
>
|
||||
{filteredVenues.map(v => <option key={v.name} value={v.name}>{v.name} {v.capacity ? `(Max: ${v.capacity})` : ''}</option>)}
|
||||
@@ -920,7 +974,7 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
|
||||
<input
|
||||
type="text" required
|
||||
value={formData.customVenue}
|
||||
onChange={e => setFormData({...formData, customVenue: e.target.value})}
|
||||
onChange={e => setFormData({ ...formData, customVenue: e.target.value })}
|
||||
className="w-full bg-slate-50 border border-brand-indigo/30 rounded-2xl py-4 px-6 text-sm font-semibold focus:bg-white focus:border-brand-indigo transition-all"
|
||||
placeholder="Specify custom venue location..."
|
||||
/>
|
||||
@@ -938,7 +992,7 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
|
||||
<input
|
||||
type="datetime-local" required
|
||||
value={formData.startDate}
|
||||
onChange={e => setFormData({...formData, startDate: e.target.value})}
|
||||
onChange={e => setFormData({ ...formData, startDate: e.target.value })}
|
||||
className="w-full bg-slate-50 border border-transparent rounded-2xl py-4 px-6 text-sm font-semibold focus:bg-white focus:border-brand-indigo/30 transition-all"
|
||||
/>
|
||||
</div>
|
||||
@@ -948,7 +1002,7 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
|
||||
<input
|
||||
type="datetime-local" required
|
||||
value={formData.endDate}
|
||||
onChange={e => setFormData({...formData, endDate: e.target.value})}
|
||||
onChange={e => setFormData({ ...formData, endDate: e.target.value })}
|
||||
className="w-full bg-slate-50 border border-transparent rounded-2xl py-4 px-6 text-sm font-semibold focus:bg-white focus:border-brand-indigo/30 transition-all"
|
||||
/>
|
||||
</div>
|
||||
@@ -962,7 +1016,7 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
|
||||
placeholder="Unlimited"
|
||||
className="w-full border border-transparent rounded-2xl py-4 px-6 text-sm font-semibold transition-all focus:bg-white bg-slate-50 text-text-dark focus:border-brand-indigo/30"
|
||||
value={formData.maxParticipants}
|
||||
onChange={e => setFormData({...formData, maxParticipants: e.target.value})}
|
||||
onChange={e => setFormData({ ...formData, maxParticipants: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -983,7 +1037,7 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
|
||||
<input
|
||||
type="number"
|
||||
value={formData.budget}
|
||||
onChange={e => setFormData({...formData, budget: e.target.value})}
|
||||
onChange={e => setFormData({ ...formData, budget: e.target.value })}
|
||||
className="w-full bg-slate-50 border border-transparent rounded-2xl py-4 pl-12 pr-6 text-sm font-semibold focus:bg-white focus:border-brand-indigo/30 transition-all"
|
||||
placeholder="0.00"
|
||||
/>
|
||||
@@ -997,7 +1051,7 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
|
||||
<input
|
||||
type="number"
|
||||
value={formData.refreshment_expense}
|
||||
onChange={e => setFormData({...formData, refreshment_expense: e.target.value})}
|
||||
onChange={e => setFormData({ ...formData, refreshment_expense: e.target.value })}
|
||||
className="w-full bg-slate-50 border border-transparent rounded-2xl py-4 pl-12 pr-6 text-sm font-semibold focus:bg-white focus:border-brand-indigo/30 transition-all"
|
||||
placeholder="0.00"
|
||||
/>
|
||||
@@ -1011,7 +1065,7 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
|
||||
<input
|
||||
type="number"
|
||||
value={formData.transportation_expense}
|
||||
onChange={e => setFormData({...formData, transportation_expense: e.target.value})}
|
||||
onChange={e => setFormData({ ...formData, transportation_expense: e.target.value })}
|
||||
className="w-full bg-slate-50 border border-transparent rounded-2xl py-4 pl-12 pr-6 text-sm font-semibold focus:bg-white focus:border-brand-indigo/30 transition-all"
|
||||
placeholder="0.00"
|
||||
/>
|
||||
@@ -1025,7 +1079,7 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
|
||||
<input
|
||||
type="number"
|
||||
value={formData.session_coverage_fee}
|
||||
onChange={e => setFormData({...formData, session_coverage_fee: e.target.value})}
|
||||
onChange={e => setFormData({ ...formData, session_coverage_fee: e.target.value })}
|
||||
className="w-full bg-slate-50 border border-transparent rounded-2xl py-4 pl-12 pr-6 text-sm font-semibold focus:bg-white focus:border-brand-indigo/30 transition-all"
|
||||
placeholder="0.00"
|
||||
/>
|
||||
@@ -1048,7 +1102,7 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
|
||||
<div className="space-y-3">
|
||||
<label className="text-[10px] font-black uppercase tracking-widest text-text-muted block">Registration Fee Policy</label>
|
||||
<div
|
||||
onClick={() => setFormData({...formData, hasRegistrationFee: !formData.hasRegistrationFee})}
|
||||
onClick={() => setFormData({ ...formData, hasRegistrationFee: !formData.hasRegistrationFee })}
|
||||
className="flex items-center justify-between p-4 bg-slate-50 rounded-2xl border border-transparent hover:border-brand-indigo/20 cursor-pointer transition-all group"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -1093,7 +1147,7 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
|
||||
<input
|
||||
type="number"
|
||||
value={formData.registrationFee}
|
||||
onChange={e => setFormData({...formData, registrationFee: e.target.value})}
|
||||
onChange={e => setFormData({ ...formData, registrationFee: e.target.value })}
|
||||
className="w-full bg-white border border-brand-indigo/20 rounded-xl py-4 pl-12 pr-6 text-sm font-bold text-brand-indigo focus:ring-2 ring-brand-indigo/20 outline-none"
|
||||
placeholder="Amount in INR"
|
||||
/>
|
||||
@@ -1105,7 +1159,7 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
|
||||
<input
|
||||
type="url"
|
||||
value={formData.paymentLink}
|
||||
onChange={e => setFormData({...formData, paymentLink: e.target.value})}
|
||||
onChange={e => setFormData({ ...formData, paymentLink: e.target.value })}
|
||||
className="w-full bg-white border border-brand-indigo/20 rounded-xl py-4 px-6 text-sm font-bold text-brand-indigo focus:ring-2 ring-brand-indigo/20 outline-none"
|
||||
placeholder="https://forms.gle/... or payment link"
|
||||
/>
|
||||
@@ -1211,7 +1265,7 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
|
||||
<select
|
||||
required
|
||||
value={formData.centreName}
|
||||
onChange={e => setFormData({...formData, centreName: e.target.value})}
|
||||
onChange={e => setFormData({ ...formData, centreName: e.target.value })}
|
||||
className="w-full bg-slate-50 border border-transparent rounded-2xl py-4 px-6 text-sm font-semibold appearance-none cursor-pointer group-hover:bg-slate-100 transition-all pr-12"
|
||||
>
|
||||
{(CENTRE_MAPPING[user?.department || ''] || CENTRE_MAPPING['Placement Department'] || []).map(centre => (
|
||||
@@ -1226,7 +1280,7 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
|
||||
<div className="flex bg-slate-50 p-2 rounded-2xl gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setFormData({...formData, isPublicEvent: false})}
|
||||
onClick={() => setFormData({ ...formData, isPublicEvent: false })}
|
||||
className={cn(
|
||||
"flex-1 py-3 rounded-xl text-[10px] font-black uppercase tracking-widest transition-all",
|
||||
!formData.isPublicEvent ? "bg-white text-brand-indigo shadow-sm" : "text-text-muted hover:bg-slate-100"
|
||||
@@ -1236,7 +1290,7 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setFormData({...formData, isPublicEvent: true})}
|
||||
onClick={() => setFormData({ ...formData, isPublicEvent: true })}
|
||||
className={cn(
|
||||
"flex-1 py-3 rounded-xl text-[10px] font-black uppercase tracking-widest transition-all",
|
||||
formData.isPublicEvent ? "bg-white text-brand-indigo shadow-sm" : "text-text-muted hover:bg-slate-100"
|
||||
@@ -1252,159 +1306,159 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
|
||||
<div className="space-y-8">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
<div className={cn((eventScope === 'INSTITUTIONAL' || eventScope === 'CLUB' || eventScope === 'PLACEMENT' || (eventScope === 'DEPARTMENT' && isPlacementCell)) && "md:col-span-2")}>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<label className="text-[10px] font-black uppercase tracking-widest text-text-muted block">
|
||||
{(eventScope === 'INSTITUTIONAL' || eventScope === 'CLUB' || eventScope === 'PLACEMENT' || (eventScope === 'DEPARTMENT' && isPlacementCell)) ? 'Target Departments' : 'Proposing Department'}
|
||||
</label>
|
||||
{(eventScope === 'INSTITUTIONAL' || eventScope === 'CLUB' || eventScope === 'PLACEMENT' || (eventScope === 'DEPARTMENT' && isPlacementCell)) && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const allD = formData.institution === 'RIT' ? ritDepartments : rsbDepartments;
|
||||
const isAllSelected = formData.targetDepartments.length === allD.length;
|
||||
setFormData({
|
||||
...formData,
|
||||
allDepts: !isAllSelected,
|
||||
targetDepartments: !isAllSelected ? allD : []
|
||||
});
|
||||
}}
|
||||
className={cn(
|
||||
"px-3 py-1 rounded-lg text-[8px] font-black uppercase tracking-widest border transition-all",
|
||||
formData.targetDepartments.length === (formData.institution === 'RIT' ? ritDepartments : rsbDepartments).length ? "bg-brand-indigo text-white border-brand-indigo" : "bg-white text-text-muted border-slate-200"
|
||||
)}
|
||||
>
|
||||
{formData.targetDepartments.length === (formData.institution === 'RIT' ? ritDepartments : rsbDepartments).length ? 'All Departments Selected' : 'Select All Departments'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{(eventScope === 'INSTITUTIONAL' || eventScope === 'CLUB' || eventScope === 'PLACEMENT' || (eventScope === 'DEPARTMENT' && isPlacementCell)) ? (
|
||||
<div className="flex flex-wrap gap-2 p-6 bg-slate-50 rounded-[2.5rem] border border-slate-100">
|
||||
{(formData.institution === 'RIT' ? ritDepartments : rsbDepartments).map(dept => (
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<label className="text-[10px] font-black uppercase tracking-widest text-text-muted block">
|
||||
{(eventScope === 'INSTITUTIONAL' || eventScope === 'CLUB' || eventScope === 'PLACEMENT' || (eventScope === 'DEPARTMENT' && isPlacementCell)) ? 'Target Departments' : 'Proposing Department'}
|
||||
</label>
|
||||
{(eventScope === 'INSTITUTIONAL' || eventScope === 'CLUB' || eventScope === 'PLACEMENT' || (eventScope === 'DEPARTMENT' && isPlacementCell)) && (
|
||||
<button
|
||||
key={dept}
|
||||
type="button"
|
||||
onClick={() => toggleSelection('targetDepartments', dept)}
|
||||
onClick={() => {
|
||||
const allD = formData.institution === 'RIT' ? ritDepartments : rsbDepartments;
|
||||
const isAllSelected = formData.targetDepartments.length === allD.length;
|
||||
setFormData({
|
||||
...formData,
|
||||
allDepts: !isAllSelected,
|
||||
targetDepartments: !isAllSelected ? allD : []
|
||||
});
|
||||
}}
|
||||
className={cn(
|
||||
"px-4 py-2 rounded-xl text-[10px] font-black uppercase tracking-widest transition-all border",
|
||||
formData.targetDepartments.includes(dept)
|
||||
? "bg-brand-navy text-white border-brand-navy shadow-md"
|
||||
: "bg-white text-text-muted border-slate-200 hover:border-brand-indigo/30"
|
||||
"px-3 py-1 rounded-lg text-[8px] font-black uppercase tracking-widest border transition-all",
|
||||
formData.targetDepartments.length === (formData.institution === 'RIT' ? ritDepartments : rsbDepartments).length ? "bg-brand-indigo text-white border-brand-indigo" : "bg-white text-text-muted border-slate-200"
|
||||
)}
|
||||
>
|
||||
{dept}
|
||||
{formData.targetDepartments.length === (formData.institution === 'RIT' ? ritDepartments : rsbDepartments).length ? 'All Departments Selected' : 'Select All Departments'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-4 bg-slate-50 rounded-2xl border border-slate-100 text-sm font-bold text-text-dark">
|
||||
{formData.department}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={cn((eventScope === 'INSTITUTIONAL' || eventScope === 'CLUB' || eventScope === 'PLACEMENT' || (eventScope === 'DEPARTMENT' && isPlacementCell)) && "md:col-span-2")}>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<label className="text-[10px] font-black uppercase tracking-widest text-text-muted block">
|
||||
{(eventScope === 'INSTITUTIONAL' || eventScope === 'CLUB' || eventScope === 'PLACEMENT' || (eventScope === 'DEPARTMENT' && isPlacementCell)) ? 'Target Batches' : 'Target Batch'}
|
||||
</label>
|
||||
{(eventScope === 'INSTITUTIONAL' || eventScope === 'CLUB' || eventScope === 'PLACEMENT' || (eventScope === 'DEPARTMENT' && isPlacementCell)) && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const isAllSelected = formData.academicYears.length === availableYears.length;
|
||||
setFormData({
|
||||
...formData,
|
||||
allBatches: !isAllSelected,
|
||||
academicYears: !isAllSelected ? availableYears : []
|
||||
});
|
||||
}}
|
||||
className={cn(
|
||||
"px-3 py-1 rounded-lg text-[8px] font-black uppercase tracking-widest border transition-all",
|
||||
formData.academicYears.length === availableYears.length ? "bg-brand-indigo text-white border-brand-indigo" : "bg-white text-text-muted border-slate-200"
|
||||
)}
|
||||
>
|
||||
{formData.academicYears.length === availableYears.length ? 'All Batches Selected' : 'Select All Batches'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{(eventScope === 'INSTITUTIONAL' || eventScope === 'CLUB' || eventScope === 'PLACEMENT' || (eventScope === 'DEPARTMENT' && isPlacementCell)) ? (
|
||||
<div className="flex flex-wrap gap-2 p-6 bg-slate-50 rounded-[2.5rem] border border-slate-100">
|
||||
{availableYears.map(year => (
|
||||
<button
|
||||
key={year}
|
||||
type="button"
|
||||
onClick={() => toggleSelection('academicYears', year)}
|
||||
className={cn(
|
||||
"px-4 py-2 rounded-xl text-[10px] font-black uppercase tracking-widest transition-all border",
|
||||
formData.academicYears.includes(year)
|
||||
? "bg-brand-navy text-white border-brand-navy shadow-md"
|
||||
: "bg-white text-text-muted border-slate-200 hover:border-brand-indigo/30"
|
||||
)}
|
||||
>
|
||||
{year}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<select
|
||||
value={formData.academicYears[0] || ''}
|
||||
onChange={e => setFormData({...formData, targetedBatch: '', academicYears: [e.target.value], targetedSections: [], allSections: false})}
|
||||
className="w-full bg-slate-50 border border-transparent rounded-2xl py-4 px-6 text-sm font-semibold appearance-none"
|
||||
>
|
||||
<option value="">Select Academic Year</option>
|
||||
{availableYears.map(year => <option key={year} value={year}>{year}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{(eventScope === 'DEPARTMENT' || eventScope === 'PLACEMENT') && formData.academicYears.length > 0 && (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-[10px] font-black uppercase tracking-widest text-text-muted block">Select Sections</label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const newVal = !formData.allSections;
|
||||
setFormData({
|
||||
...formData,
|
||||
allSections: newVal,
|
||||
targetedSections: newVal ? availableSections.filter(s => !isSectionDisabled(s)) : []
|
||||
});
|
||||
if (newVal) setShowAlternateEvent(false);
|
||||
}}
|
||||
className={cn(
|
||||
"px-3 py-1 rounded-lg text-[8px] font-black uppercase tracking-widest border transition-all",
|
||||
formData.allSections ? "bg-brand-indigo text-white border-brand-indigo" : "bg-white text-text-muted border-slate-200"
|
||||
)}
|
||||
>
|
||||
{formData.allSections ? 'All Sections Selected' : 'Select All Sections'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{(eventScope === 'INSTITUTIONAL' || eventScope === 'CLUB' || eventScope === 'PLACEMENT' || (eventScope === 'DEPARTMENT' && isPlacementCell)) ? (
|
||||
<div className="flex flex-wrap gap-2 p-6 bg-slate-50 rounded-[2.5rem] border border-slate-100">
|
||||
{(formData.institution === 'RIT' ? ritDepartments : rsbDepartments).map(dept => (
|
||||
<button
|
||||
key={dept}
|
||||
type="button"
|
||||
onClick={() => toggleSelection('targetDepartments', dept)}
|
||||
className={cn(
|
||||
"px-4 py-2 rounded-xl text-[10px] font-black uppercase tracking-widest transition-all border",
|
||||
formData.targetDepartments.includes(dept)
|
||||
? "bg-brand-navy text-white border-brand-navy shadow-md"
|
||||
: "bg-white text-text-muted border-slate-200 hover:border-brand-indigo/30"
|
||||
)}
|
||||
>
|
||||
{dept}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-4 bg-slate-50 rounded-2xl border border-slate-100 text-sm font-bold text-text-dark">
|
||||
{formData.department}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{availableSections.map(section => {
|
||||
const disabled = isSectionDisabled(section);
|
||||
return (
|
||||
<div className={cn((eventScope === 'INSTITUTIONAL' || eventScope === 'CLUB' || eventScope === 'PLACEMENT' || (eventScope === 'DEPARTMENT' && isPlacementCell)) && "md:col-span-2")}>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<label className="text-[10px] font-black uppercase tracking-widest text-text-muted block">
|
||||
{(eventScope === 'INSTITUTIONAL' || eventScope === 'CLUB' || eventScope === 'PLACEMENT' || (eventScope === 'DEPARTMENT' && isPlacementCell)) ? 'Target Batches' : 'Target Batch'}
|
||||
</label>
|
||||
{(eventScope === 'INSTITUTIONAL' || eventScope === 'CLUB' || eventScope === 'PLACEMENT' || (eventScope === 'DEPARTMENT' && isPlacementCell)) && (
|
||||
<button
|
||||
key={section}
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
onClick={() => toggleSelection('targetedSections', section)}
|
||||
onClick={() => {
|
||||
const isAllSelected = formData.academicYears.length === availableYears.length;
|
||||
setFormData({
|
||||
...formData,
|
||||
allBatches: !isAllSelected,
|
||||
academicYears: !isAllSelected ? availableYears : []
|
||||
});
|
||||
}}
|
||||
className={cn(
|
||||
"w-12 h-12 rounded-xl flex items-center justify-center text-sm font-black transition-all border",
|
||||
formData.targetedSections.includes(section) ? "bg-brand-indigo text-white" : disabled ? "bg-slate-100 text-slate-300 cursor-not-allowed" : "bg-slate-50 text-text-muted"
|
||||
"px-3 py-1 rounded-lg text-[8px] font-black uppercase tracking-widest border transition-all",
|
||||
formData.academicYears.length === availableYears.length ? "bg-brand-indigo text-white border-brand-indigo" : "bg-white text-text-muted border-slate-200"
|
||||
)}
|
||||
>
|
||||
{section}
|
||||
{formData.academicYears.length === availableYears.length ? 'All Batches Selected' : 'Select All Batches'}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
)}
|
||||
</div>
|
||||
|
||||
{(eventScope === 'INSTITUTIONAL' || eventScope === 'CLUB' || eventScope === 'PLACEMENT' || (eventScope === 'DEPARTMENT' && isPlacementCell)) ? (
|
||||
<div className="flex flex-wrap gap-2 p-6 bg-slate-50 rounded-[2.5rem] border border-slate-100">
|
||||
{availableYears.map(year => (
|
||||
<button
|
||||
key={year}
|
||||
type="button"
|
||||
onClick={() => toggleSelection('academicYears', year)}
|
||||
className={cn(
|
||||
"px-4 py-2 rounded-xl text-[10px] font-black uppercase tracking-widest transition-all border",
|
||||
formData.academicYears.includes(year)
|
||||
? "bg-brand-navy text-white border-brand-navy shadow-md"
|
||||
: "bg-white text-text-muted border-slate-200 hover:border-brand-indigo/30"
|
||||
)}
|
||||
>
|
||||
{year}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<select
|
||||
value={formData.academicYears[0] || ''}
|
||||
onChange={e => setFormData({ ...formData, targetedBatch: '', academicYears: [e.target.value], targetedSections: [], allSections: false })}
|
||||
className="w-full bg-slate-50 border border-transparent rounded-2xl py-4 px-6 text-sm font-semibold appearance-none"
|
||||
>
|
||||
<option value="">Select Academic Year</option>
|
||||
{availableYears.map(year => <option key={year} value={year}>{year}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{(eventScope === 'DEPARTMENT' || eventScope === 'PLACEMENT') && formData.academicYears.length > 0 && (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-[10px] font-black uppercase tracking-widest text-text-muted block">Select Sections</label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const newVal = !formData.allSections;
|
||||
setFormData({
|
||||
...formData,
|
||||
allSections: newVal,
|
||||
targetedSections: newVal ? availableSections.filter(s => !isSectionDisabled(s)) : []
|
||||
});
|
||||
if (newVal) setShowAlternateEvent(false);
|
||||
}}
|
||||
className={cn(
|
||||
"px-3 py-1 rounded-lg text-[8px] font-black uppercase tracking-widest border transition-all",
|
||||
formData.allSections ? "bg-brand-indigo text-white border-brand-indigo" : "bg-white text-text-muted border-slate-200"
|
||||
)}
|
||||
>
|
||||
{formData.allSections ? 'All Sections Selected' : 'Select All Sections'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{availableSections.map(section => {
|
||||
const disabled = isSectionDisabled(section);
|
||||
return (
|
||||
<button
|
||||
key={section}
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
onClick={() => toggleSelection('targetedSections', section)}
|
||||
className={cn(
|
||||
"w-12 h-12 rounded-xl flex items-center justify-center text-sm font-black transition-all border",
|
||||
formData.targetedSections.includes(section) ? "bg-brand-indigo text-white" : disabled ? "bg-slate-100 text-slate-300 cursor-not-allowed" : "bg-slate-50 text-text-muted"
|
||||
)}
|
||||
>
|
||||
{section}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{formData.targetedSections.length > 0 && (
|
||||
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} className="p-6 bg-slate-50 rounded-3xl space-y-4 border border-slate-100">
|
||||
@@ -1416,7 +1470,7 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
|
||||
{!formData.allSections && !showAlternateEvent && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { setShowAlternateEvent(true); setAlternateEvent({...alternateEvent, eventName: `${formData.eventName} (Division 2)`, startDate: formData.startDate, endDate: formData.endDate}); }}
|
||||
onClick={() => { setShowAlternateEvent(true); setAlternateEvent({ ...alternateEvent, eventName: `${formData.eventName} (Division 2)`, startDate: formData.startDate, endDate: formData.endDate }); }}
|
||||
className="px-4 py-2 bg-brand-indigo text-white rounded-xl text-[8px] font-black uppercase tracking-widest"
|
||||
>
|
||||
Add Alternate Session
|
||||
@@ -1457,13 +1511,13 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
|
||||
<X className="w-4 h-4 text-red-500 cursor-pointer" onClick={() => setShowAlternateEvent(false)} />
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<input type="text" placeholder="Session Name" value={alternateEvent.eventName} onChange={e => setAlternateEvent({...alternateEvent, eventName: e.target.value})} className="w-full bg-white border border-slate-100 rounded-xl py-3 px-4 text-xs font-bold" />
|
||||
<input type="text" placeholder="Session Name" value={alternateEvent.eventName} onChange={e => setAlternateEvent({ ...alternateEvent, eventName: e.target.value })} className="w-full bg-white border border-slate-100 rounded-xl py-3 px-4 text-xs font-bold" />
|
||||
<div className="w-full">
|
||||
<select value={alternateEvent.venue} onChange={e => setAlternateEvent({...alternateEvent, venue: e.target.value})} className="w-full bg-white border border-slate-100 rounded-xl py-3 px-4 text-xs font-bold">
|
||||
<select value={alternateEvent.venue} onChange={e => setAlternateEvent({ ...alternateEvent, venue: e.target.value })} className="w-full bg-white border border-slate-100 rounded-xl py-3 px-4 text-xs font-bold">
|
||||
{filteredVenues.map(v => <option key={v.name} value={v.name}>{v.name} {v.capacity ? `(Max: ${v.capacity})` : ''}</option>)}
|
||||
</select>
|
||||
{alternateEvent.venue === 'Others.' && (
|
||||
<input type="text" required placeholder="Specify custom venue..." value={alternateEvent.customVenue} onChange={e => setAlternateEvent({...alternateEvent, customVenue: e.target.value})} className="w-full mt-3 bg-white border border-brand-indigo/30 rounded-xl py-3 px-4 text-xs font-bold" />
|
||||
<input type="text" required placeholder="Specify custom venue..." value={alternateEvent.customVenue} onChange={e => setAlternateEvent({ ...alternateEvent, customVenue: e.target.value })} className="w-full mt-3 bg-white border border-brand-indigo/30 rounded-xl py-3 px-4 text-xs font-bold" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -65,6 +65,36 @@ export const ManageNoticesView: React.FC = () => {
|
||||
const [eventLink, setEventLink] = useState('');
|
||||
const [eventImage, setEventImage] = useState<string | null>(null);
|
||||
|
||||
const compressImage = (base64Str: string): Promise<string> => {
|
||||
return new Promise((resolve) => {
|
||||
const img = new Image();
|
||||
img.src = base64Str;
|
||||
img.onload = () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
const MAX_WIDTH = 800;
|
||||
const MAX_HEIGHT = 600;
|
||||
let width = img.width;
|
||||
let height = img.height;
|
||||
if (width > height) {
|
||||
if (width > MAX_WIDTH) {
|
||||
height *= MAX_WIDTH / width;
|
||||
width = MAX_WIDTH;
|
||||
}
|
||||
} else {
|
||||
if (height > MAX_HEIGHT) {
|
||||
width *= MAX_HEIGHT / height;
|
||||
height = MAX_HEIGHT;
|
||||
}
|
||||
}
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx?.drawImage(img, 0, 0, width, height);
|
||||
resolve(canvas.toDataURL('image/jpeg', 0.7));
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const handleEventImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
@@ -73,8 +103,9 @@ export const ManageNoticesView: React.FC = () => {
|
||||
return;
|
||||
}
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
setEventImage(reader.result as string);
|
||||
reader.onloadend = async () => {
|
||||
const compressed = await compressImage(reader.result as string);
|
||||
setEventImage(compressed);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
@@ -133,8 +164,9 @@ export const ManageNoticesView: React.FC = () => {
|
||||
return;
|
||||
}
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
setNoticeImage(reader.result as string);
|
||||
reader.onloadend = async () => {
|
||||
const compressed = await compressImage(reader.result as string);
|
||||
setNoticeImage(compressed);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
@@ -163,6 +195,9 @@ export const ManageNoticesView: React.FC = () => {
|
||||
setIsNoticeModalOpen(false);
|
||||
fetchInitialData();
|
||||
showAlert('Success', 'Notice posted successfully to Campus Notice Board.', 'success');
|
||||
} else {
|
||||
const errData = await response.json();
|
||||
showAlert('Error', errData.message || 'Failed to save notice. Image may be too large.', 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to post notice:', err);
|
||||
@@ -222,6 +257,9 @@ export const ManageNoticesView: React.FC = () => {
|
||||
setEventImage(null);
|
||||
fetchInitialData();
|
||||
showAlert('Success', 'Special event added to the registry successfully.', 'success');
|
||||
} else {
|
||||
const errData = await response.json();
|
||||
showAlert('Error', errData.message || 'Failed to create special event. Image may be too large.', 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Post failed:', err);
|
||||
|
||||
@@ -8,7 +8,7 @@ interface CategoryGridProps {
|
||||
}
|
||||
|
||||
const CategoryGrid: React.FC<CategoryGridProps> = ({ onSelectCategory }) => {
|
||||
const threeCategories = CATEGORIES.filter(c => c.id !== 'CENTRE-ACTIVITY' && c.id !== 'WORKWORK' && c.id !== 'WORKSHOP');
|
||||
const threeCategories = CATEGORIES.filter(c => c.id !== 'CENTRE-ACTIVITY');
|
||||
const centreActivityCategory = CATEGORIES.find(c => c.id === 'CENTRE-ACTIVITY');
|
||||
|
||||
return (
|
||||
@@ -18,7 +18,7 @@ const CategoryGrid: React.FC<CategoryGridProps> = ({ onSelectCategory }) => {
|
||||
</h2>
|
||||
|
||||
{/* Three Standard Categories */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-2 gap-8 max-w-5xl mx-auto mb-8">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 max-w-7xl mx-auto mb-8">
|
||||
{threeCategories.map((cat) => (
|
||||
<div
|
||||
key={cat.id}
|
||||
|
||||
@@ -31,13 +31,12 @@ const EventList: React.FC<EventListProps> = ({
|
||||
const [selectedDomainName, setSelectedDomainName] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedCategory === 'TECHNICAL' || selectedCategory === 'NON-TECHNICAL') {
|
||||
setSelectedDomain('ALL');
|
||||
setSelectedDomainName(selectedCategory === 'TECHNICAL' ? 'Technical Events' : 'Non-Technical Events');
|
||||
} else {
|
||||
setSelectedDomain(null);
|
||||
setSelectedDomainName(null);
|
||||
}
|
||||
setSelectedDomain('ALL');
|
||||
if (selectedCategory === 'TECHNICAL') setSelectedDomainName('Technical Events');
|
||||
else if (selectedCategory === 'NON-TECHNICAL') setSelectedDomainName('Non-Technical Events');
|
||||
else if (selectedCategory === 'WORKSHOP') setSelectedDomainName('Workshops');
|
||||
else if (selectedCategory === 'CENTRE-ACTIVITY') setSelectedDomainName('Centre Activities');
|
||||
else setSelectedDomainName('All Events');
|
||||
}, [selectedCategory]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -82,17 +81,11 @@ const EventList: React.FC<EventListProps> = ({
|
||||
return (
|
||||
<div className="pt-40 pb-16 px-6 md:px-12 lg:px-24 animate-in fade-in duration-500">
|
||||
<button
|
||||
onClick={() => {
|
||||
if (selectedCategory === 'TECHNICAL' || selectedCategory === 'NON-TECHNICAL') {
|
||||
onSelectCategory(null);
|
||||
} else {
|
||||
setSelectedDomain(null);
|
||||
}
|
||||
}}
|
||||
onClick={() => onSelectCategory(null)}
|
||||
className="flex items-center gap-2 text-[#f97316] font-black uppercase tracking-widest mb-10 group hover:translate-x-[-5px] transition-transform"
|
||||
>
|
||||
<i className="fas fa-arrow-left"></i>
|
||||
{(selectedCategory === 'TECHNICAL' || selectedCategory === 'NON-TECHNICAL') ? 'BACK TO CATEGORIES' : 'BACK TO DOMAINS'}
|
||||
BACK TO CATEGORIES
|
||||
</button>
|
||||
|
||||
<div className="flex flex-col md:flex-row md:items-end justify-between mb-12 gap-6">
|
||||
|
||||
@@ -103,7 +103,17 @@ const EventsHeroSlider: React.FC<EventsHeroSliderProps> = ({ events }) => {
|
||||
<div className="flex items-center gap-4 mb-6">
|
||||
<div className="w-12 h-[2px] bg-[#f97316]"></div>
|
||||
<span className="text-white font-bold tracking-[0.3em] uppercase text-sm">
|
||||
JOIN US <span className="text-[#f97316] ml-2">{event.date}</span>
|
||||
JOIN US <span className="text-[#f97316] ml-2">
|
||||
{new Date(event.date).toLocaleString('en-IN', {
|
||||
timeZone: 'Asia/Kolkata',
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false
|
||||
}) + ' IST'}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ const HomeDashboard: React.FC<HomeDashboardProps> = ({
|
||||
settings = {}
|
||||
}) => {
|
||||
const [now, setNow] = useState(new Date());
|
||||
const [zoomedImage, setZoomedImage] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => setNow(new Date()), 30000);
|
||||
@@ -129,11 +130,24 @@ const HomeDashboard: React.FC<HomeDashboardProps> = ({
|
||||
</h3>
|
||||
|
||||
{(ann as any).image && (
|
||||
<img
|
||||
src={(ann as any).image}
|
||||
alt={ann.title}
|
||||
className="w-full h-40 object-cover rounded-xl mb-4 shrink-0 shadow-sm border border-black/10"
|
||||
/>
|
||||
<div className="relative rounded-xl overflow-hidden mb-4 shrink-0 shadow-sm border border-black/10 group/img">
|
||||
<img
|
||||
src={(ann as any).image}
|
||||
alt={ann.title}
|
||||
className="w-full h-40 object-cover"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setZoomedImage((ann as any).image);
|
||||
}}
|
||||
className="absolute top-2 right-2 bg-black/70 hover:bg-black text-white w-7 h-7 rounded-full opacity-100 sm:opacity-0 group-hover/img:opacity-100 transition-opacity duration-200 z-20 flex items-center justify-center shadow-md cursor-pointer border border-white/20"
|
||||
title="Inspect Image"
|
||||
>
|
||||
<i className="fas fa-search-plus text-xs"></i>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="font-sans text-gray-800 text-base flex-grow mb-6 leading-relaxed opacity-90">
|
||||
@@ -174,6 +188,28 @@ const HomeDashboard: React.FC<HomeDashboardProps> = ({
|
||||
<AccreditationsSection />
|
||||
|
||||
<SpecialEventsBanner specialEvents={specialEvents} />
|
||||
|
||||
{/* Image Zoom Modal */}
|
||||
{zoomedImage && (
|
||||
<div
|
||||
className="fixed inset-0 z-[99999] flex items-center justify-center p-4 bg-black/90 backdrop-blur-sm animate-in fade-in duration-200"
|
||||
onClick={() => setZoomedImage(null)}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute top-6 right-6 text-white/70 hover:text-white text-2xl bg-white/10 hover:bg-white/20 w-12 h-12 rounded-full flex items-center justify-center transition-all cursor-pointer border border-white/10"
|
||||
onClick={() => setZoomedImage(null)}
|
||||
>
|
||||
<i className="fas fa-times"></i>
|
||||
</button>
|
||||
<img
|
||||
src={zoomedImage}
|
||||
alt="Inspected Notice"
|
||||
className="max-w-full max-h-[90vh] object-contain rounded-lg shadow-2xl animate-in zoom-in-95 duration-200"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -224,24 +224,7 @@ const ProfileView: React.FC<ProfileViewProps> = ({ onLogout }) => {
|
||||
<div className="bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden sticky top-32">
|
||||
<div className="h-32 bg-gradient-to-r from-orange-400 to-rose-500"></div>
|
||||
<div className="px-6 pb-8">
|
||||
<div className="relative -mt-16 mb-6 flex justify-center">
|
||||
<div className="w-32 h-32 rounded-full border-4 border-white shadow-lg overflow-hidden bg-gray-100 relative group">
|
||||
<img
|
||||
src={avatarUrl || `https://api.dicebear.com/7.x/avataaars/svg?seed=${user?.fullName}`}
|
||||
alt="Profile"
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
{isEditing && (
|
||||
<div
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
className="absolute inset-0 bg-black/50 flex items-center justify-center cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
>
|
||||
<i className="fas fa-camera text-white text-xl"></i>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<input type="file" ref={fileInputRef} className="hidden" onChange={handlePhotoChange} accept="image/*" />
|
||||
</div>
|
||||
|
||||
|
||||
<div className="text-center mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-1">{user?.fullName}</h2>
|
||||
|
||||
@@ -23,12 +23,12 @@ const SpecialEventsBanner: React.FC<SpecialEventsBannerProps> = ({ specialEvents
|
||||
const displayEvents = isLooping ? loopEvents : specialEvents;
|
||||
|
||||
const getCardWidth = () => {
|
||||
if (isLooping) return '300px';
|
||||
if (isLooping) return '330px';
|
||||
const count = specialEvents.length;
|
||||
if (count === 1) return '480px';
|
||||
if (count === 2) return '400px';
|
||||
if (count === 3) return '340px';
|
||||
return '290px'; // 4 events
|
||||
if (count === 1) return '510px';
|
||||
if (count === 2) return '430px';
|
||||
if (count === 3) return '370px';
|
||||
return '310px'; // 4 events
|
||||
};
|
||||
const cardWidth = getCardWidth();
|
||||
|
||||
@@ -110,13 +110,10 @@ const SpecialEventsBanner: React.FC<SpecialEventsBannerProps> = ({ specialEvents
|
||||
}
|
||||
|
||||
.seb-card-img {
|
||||
transition: transform 0.7s ease;
|
||||
will-change: transform;
|
||||
backface-visibility: hidden;
|
||||
-webkit-backface-visibility: hidden;
|
||||
transition: transform 0.7s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
}
|
||||
.seb-card:hover .seb-card-img {
|
||||
transform: skewX(5deg) skewY(3deg) scale(1.35) translateZ(0) !important;
|
||||
transform: skewX(5deg) skewY(3deg) scale(1.35) !important;
|
||||
}
|
||||
.seb-card:hover {
|
||||
box-shadow: 0 0 50px 6px rgba(250,204,21,0.20);
|
||||
@@ -203,7 +200,7 @@ const SpecialEventsBanner: React.FC<SpecialEventsBannerProps> = ({ specialEvents
|
||||
{/* ── CARD SLIDER (Slanted, inherits skewY(-3deg) from parent) ── */}
|
||||
<div
|
||||
className="seb-slider-container"
|
||||
style={{ padding: '16px 0 20px' }}
|
||||
style={{ padding: '40px 0 40px' }}
|
||||
>
|
||||
<div className={isLooping ? "seb-track" : "seb-static-track"}>
|
||||
{displayEvents.map((event, idx) => (
|
||||
@@ -214,10 +211,10 @@ const SpecialEventsBanner: React.FC<SpecialEventsBannerProps> = ({ specialEvents
|
||||
style={{
|
||||
width: cardWidth,
|
||||
maxWidth: '85vw',
|
||||
height: '400px',
|
||||
height: '440px',
|
||||
marginRight: idx === displayEvents.length - 1 ? '0px' : '-28px',
|
||||
clipPath: 'polygon(9% 0%,100% 0%,91% 100%,0% 100%)',
|
||||
transform: 'skewX(-5deg)',
|
||||
transform: `skewX(-5deg) translateY(${idx % 2 === 0 ? '20px' : '-20px'})`,
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
@@ -226,7 +223,7 @@ const SpecialEventsBanner: React.FC<SpecialEventsBannerProps> = ({ specialEvents
|
||||
className="seb-card-img absolute inset-0 bg-cover bg-center"
|
||||
style={{
|
||||
backgroundImage: 'url(' + (event.image || defaultImage) + ')',
|
||||
transform: 'skewX(5deg) skewY(3deg) scale(1.22) translateZ(0)',
|
||||
transform: 'skewX(5deg) skewY(3deg) scale(1.22)',
|
||||
}}
|
||||
/>
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ const StatsSection: React.FC<StatsSectionProps> = ({ events }) => {
|
||||
if (events) {
|
||||
const nonTech = events.filter(e => e.category === 'NON-TECHNICAL').length;
|
||||
const tech = events.filter(e => e.category === 'TECHNICAL').length;
|
||||
const workshops = events.filter(e => e.category === 'WORKSHOP').length;
|
||||
const workshops = events.filter(e => e.category === 'WORKSHOP' || e.category === 'CENTRE-ACTIVITY').length;
|
||||
|
||||
setCounts({
|
||||
nonTechnical: nonTech,
|
||||
@@ -43,7 +43,7 @@ const StatsSection: React.FC<StatsSectionProps> = ({ events }) => {
|
||||
delay: 0.2
|
||||
},
|
||||
{
|
||||
label: "Workshops",
|
||||
label: "Centre-Based Activities",
|
||||
value: counts.workshops,
|
||||
suffix: "+",
|
||||
delay: 0.3
|
||||
|
||||
@@ -32,26 +32,121 @@ const UpcomingEventsSlider: React.FC<UpcomingEventsSliderProps> = ({
|
||||
});
|
||||
}, [events]);
|
||||
|
||||
// Duplicate events multiple times to ensure the track is always filled, then double it for the 50% translation loop
|
||||
const minItems = 10;
|
||||
const repeats = Math.max(1, Math.ceil(minItems / Math.max(1, sortedEvents.length)));
|
||||
const repeatedSegment = Array(repeats).fill(sortedEvents).flat();
|
||||
const loopEvents = [...repeatedSegment, ...repeatedSegment];
|
||||
const displayEvents = useMemo(() => {
|
||||
if (sortedEvents.length === 0) return [];
|
||||
if (sortedEvents.length === 1) {
|
||||
return [
|
||||
...sortedEvents,
|
||||
{
|
||||
id: 'coming-soon-1',
|
||||
title: 'Experience Coming Soon',
|
||||
location: 'To Be Announced',
|
||||
date: 'Stay Tuned',
|
||||
category: 'UPCOMING',
|
||||
image: 'https://images.unsplash.com/photo-1504384308090-c894fdcc538d?auto=format&fit=crop&q=80&w=800',
|
||||
isComingSoon: true,
|
||||
club: 'RIT Events Team',
|
||||
coordinator: 'Hub Admins',
|
||||
event_summary: 'We are curating the next exciting experience for you. Keep an eye out for upcoming announcements and registrations!',
|
||||
status: 'APPROVED',
|
||||
pricingType: 'FREE',
|
||||
maxParticipants: 100,
|
||||
registrationDeadline: '',
|
||||
durationDays: 1,
|
||||
deptLimits: {},
|
||||
deptSectionLimits: {},
|
||||
currentParticipants: 0,
|
||||
currentDeptCounts: {},
|
||||
currentDeptSectionCounts: {},
|
||||
created_by: '',
|
||||
participantType: 'BOTH',
|
||||
verificationStatus: 'APPROVED'
|
||||
} as any,
|
||||
{
|
||||
id: 'coming-soon-2',
|
||||
title: 'More Events Unveiling Soon',
|
||||
location: 'To Be Announced',
|
||||
date: 'Stay Tuned',
|
||||
category: 'STAY TUNED',
|
||||
image: 'https://images.unsplash.com/photo-1540575467063-178a50c2df87?auto=format&fit=crop&q=80&w=800',
|
||||
isComingSoon: true,
|
||||
club: 'RIT Events Team',
|
||||
coordinator: 'Hub Admins',
|
||||
event_summary: 'We are preparing more engaging workshops and activities. Check back later for new updates!',
|
||||
status: 'APPROVED',
|
||||
pricingType: 'FREE',
|
||||
maxParticipants: 100,
|
||||
registrationDeadline: '',
|
||||
durationDays: 1,
|
||||
deptLimits: {},
|
||||
deptSectionLimits: {},
|
||||
currentParticipants: 0,
|
||||
currentDeptCounts: {},
|
||||
currentDeptSectionCounts: {},
|
||||
created_by: '',
|
||||
participantType: 'BOTH',
|
||||
verificationStatus: 'APPROVED'
|
||||
} as any
|
||||
];
|
||||
}
|
||||
if (sortedEvents.length === 2) {
|
||||
return [
|
||||
...sortedEvents,
|
||||
{
|
||||
id: 'coming-soon-1',
|
||||
title: 'Experience Coming Soon',
|
||||
location: 'To Be Announced',
|
||||
date: 'Stay Tuned',
|
||||
category: 'UPCOMING',
|
||||
image: 'https://images.unsplash.com/photo-1504384308090-c894fdcc538d?auto=format&fit=crop&q=80&w=800',
|
||||
isComingSoon: true,
|
||||
club: 'RIT Events Team',
|
||||
coordinator: 'Hub Admins',
|
||||
event_summary: 'We are curating the next exciting experience for you. Keep an eye out for upcoming announcements and registrations!',
|
||||
status: 'APPROVED',
|
||||
pricingType: 'FREE',
|
||||
maxParticipants: 100,
|
||||
registrationDeadline: '',
|
||||
durationDays: 1,
|
||||
deptLimits: {},
|
||||
deptSectionLimits: {},
|
||||
currentParticipants: 0,
|
||||
currentDeptCounts: {},
|
||||
currentDeptSectionCounts: {},
|
||||
created_by: '',
|
||||
participantType: 'BOTH',
|
||||
verificationStatus: 'APPROVED'
|
||||
} as any
|
||||
];
|
||||
}
|
||||
return sortedEvents;
|
||||
}, [sortedEvents]);
|
||||
|
||||
if (sortedEvents.length === 0) return null;
|
||||
const isMarquee = sortedEvents.length >= 3;
|
||||
|
||||
const loopEvents = useMemo(() => {
|
||||
if (!isMarquee) return displayEvents;
|
||||
const minItems = 10;
|
||||
const repeats = Math.max(1, Math.ceil(minItems / Math.max(1, displayEvents.length)));
|
||||
const repeatedSegment = Array(repeats).fill(displayEvents).flat();
|
||||
return [...repeatedSegment, ...repeatedSegment];
|
||||
}, [displayEvents, isMarquee]);
|
||||
|
||||
if (displayEvents.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="py-24 px-6 md:px-12 lg:px-24 bg-white overflow-hidden">
|
||||
<style>
|
||||
{`
|
||||
@keyframes merryGoRound {
|
||||
0% { transform: translateX(0); }
|
||||
100% { transform: translateX(-50%); }
|
||||
0% { transform: translate3d(0, 0, 0); }
|
||||
100% { transform: translate3d(-50%, 0, 0); }
|
||||
}
|
||||
.animate-merry-go-round {
|
||||
animation: merryGoRound 45s linear infinite;
|
||||
display: flex;
|
||||
width: max-content;
|
||||
will-change: transform;
|
||||
}
|
||||
.animate-merry-go-round:hover {
|
||||
animation-play-state: paused;
|
||||
@@ -67,21 +162,32 @@ const UpcomingEventsSlider: React.FC<UpcomingEventsSliderProps> = ({
|
||||
<div className="relative overflow-hidden w-full">
|
||||
{/* Gradient masks removed to eliminate fogginess */}
|
||||
|
||||
<div className="animate-merry-go-round gap-6 md:gap-8">
|
||||
<div className={`${isMarquee ? 'animate-merry-go-round' : 'flex flex-wrap justify-center'} gap-6 md:gap-8`}>
|
||||
{loopEvents.map((event, idx) => (
|
||||
<div
|
||||
key={`${event.id}-${idx}`}
|
||||
className="w-[280px] md:w-[350px] lg:w-[400px] flex-shrink-0 bg-white rounded-[2.5rem] p-5 group transition-all shadow-xl shadow-gray-900/5 hover:-translate-y-1 border border-gray-200"
|
||||
className={`w-[280px] md:w-[350px] lg:w-[400px] flex-shrink-0 bg-white rounded-[2.5rem] p-5 group transition-all shadow-xl shadow-gray-900/5 hover:-translate-y-1 border border-gray-200 transform-gpu ${
|
||||
(event as any).isComingSoon ? 'opacity-80 hover:opacity-100' : ''
|
||||
}`}
|
||||
>
|
||||
<div className="h-48 md:h-56 w-full rounded-[2rem] overflow-hidden mb-6 md:mb-8 relative">
|
||||
<div className="h-48 md:h-56 w-full rounded-[2rem] overflow-hidden mb-6 md:mb-8 relative bg-slate-900">
|
||||
<img
|
||||
src={event.image}
|
||||
alt={event.title}
|
||||
className="w-full h-full object-cover transition-transform duration-1000 group-hover:scale-110"
|
||||
className={`w-full h-full object-cover transition-transform duration-1000 ${
|
||||
(event as any).isComingSoon ? 'opacity-40 grayscale group-hover:scale-105' : 'group-hover:scale-110'
|
||||
}`}
|
||||
/>
|
||||
<div className="absolute top-4 right-4 bg-white px-4 py-2 rounded-full border border-gray-200">
|
||||
<span className="text-[9px] font-black text-[#1A202C] uppercase tracking-widest">{event.category}</span>
|
||||
</div>
|
||||
{(event as any).isComingSoon && (
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<span className="px-5 py-2.5 bg-amber-500/90 text-slate-950 text-[10px] font-black uppercase tracking-[0.2em] rounded-xl shadow-lg border border-amber-400/20 backdrop-blur-xs flex items-center gap-2">
|
||||
<i className="fas fa-lock text-[8px]"></i> Coming Soon
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="px-2 pb-2 md:pb-4">
|
||||
<h4 className="text-xl md:text-2xl font-serif text-[#1A202C] mb-4 md:mb-6 group-hover:text-[#f97316] transition-colors line-clamp-1">
|
||||
@@ -91,9 +197,21 @@ const UpcomingEventsSlider: React.FC<UpcomingEventsSliderProps> = ({
|
||||
<div className="flex items-center gap-3">
|
||||
<i className="far fa-calendar-alt text-[#f97316]"></i>
|
||||
<span className="flex items-center gap-2">
|
||||
{event.date}
|
||||
<span className="w-1 h-1 bg-gray-300 rounded-full"></span>
|
||||
<span className="text-[#f97316]">{event.schedule?.[0]?.start_time || ''}</span>
|
||||
{new Date(event.date).toLocaleString('en-IN', {
|
||||
timeZone: 'Asia/Kolkata',
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false
|
||||
}) + ' IST'}
|
||||
{event.schedule?.[0]?.start_time && (
|
||||
<>
|
||||
<span className="w-1 h-1 bg-gray-300 rounded-full"></span>
|
||||
<span className="text-[#f97316]">{event.schedule[0].start_time}</span>
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -102,12 +220,14 @@ const UpcomingEventsSlider: React.FC<UpcomingEventsSliderProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6 md:mt-8 pt-4 md:pt-6 border-t border-gray-200/30 flex justify-between items-center">
|
||||
<span className="text-[8px] md:text-[9px] text-gray-400 font-black uppercase tracking-widest">Limited Access</span>
|
||||
<span className="text-[8px] md:text-[9px] text-gray-400 font-black uppercase tracking-widest">
|
||||
{(event as any).isComingSoon ? 'Stay Tuned' : 'Limited Access'}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => setSelectedEvent(event)}
|
||||
className="text-[#1A202C] text-[9px] md:text-[10px] font-black uppercase tracking-[0.3em] hover:text-[#f97316] transition-colors flex items-center gap-2"
|
||||
className="text-[#1A202C] text-[9px] md:text-[10px] font-black uppercase tracking-[0.3em] hover:text-[#f97316] transition-colors flex items-center gap-2 cursor-pointer"
|
||||
>
|
||||
Details <i className="fas fa-arrow-right text-[8px]"></i>
|
||||
{(event as any).isComingSoon ? 'Sneak Peek' : 'Details'} <i className="fas fa-arrow-right text-[8px]"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -118,11 +238,11 @@ const UpcomingEventsSlider: React.FC<UpcomingEventsSliderProps> = ({
|
||||
|
||||
{selectedEvent && (
|
||||
<Portal>
|
||||
<div className="fixed inset-0 z-[10000] flex items-center justify-center p-4 sm:p-6 bg-slate-900/60 backdrop-blur-sm animate-in fade-in duration-300">
|
||||
<div className="bg-white rounded-[2rem] w-full max-w-lg shadow-2xl animate-in zoom-in-95 duration-500 max-h-[85vh] flex flex-col relative">
|
||||
<div className="fixed inset-0 z-[10000] flex items-center justify-center p-4 sm:p-6 bg-slate-900/60 backdrop-blur-sm animate-in fade-in duration-300" onClick={() => setSelectedEvent(null)}>
|
||||
<div className="bg-white rounded-[2rem] w-full max-w-lg shadow-2xl animate-in zoom-in-95 duration-300 max-h-[85vh] flex flex-col relative" onClick={e => e.stopPropagation()}>
|
||||
<button
|
||||
onClick={() => setSelectedEvent(null)}
|
||||
className="absolute top-6 right-6 w-8 h-8 rounded-full bg-slate-100 text-slate-500 hover:bg-slate-200 hover:text-slate-800 transition-all flex items-center justify-center z-10"
|
||||
className="absolute top-6 right-6 w-8 h-8 rounded-full bg-slate-100 text-slate-500 hover:bg-slate-200 hover:text-slate-800 transition-all flex items-center justify-center z-10 cursor-pointer"
|
||||
>
|
||||
<i className="fas fa-times text-sm"></i>
|
||||
</button>
|
||||
@@ -144,7 +264,9 @@ const UpcomingEventsSlider: React.FC<UpcomingEventsSliderProps> = ({
|
||||
<div className="flex items-center justify-between gap-2 mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-1 h-3 bg-[#f97316] rounded-full"></div>
|
||||
<h4 className="text-[10px] font-black text-slate-800 uppercase tracking-[0.2em]">Summary</h4>
|
||||
<h4 className="text-[10px] font-black text-slate-800 uppercase tracking-[0.2em]">
|
||||
{(selectedEvent as any).isComingSoon ? 'Experience Preview' : 'Summary'}
|
||||
</h4>
|
||||
</div>
|
||||
{userRole === 'STUDENT' && bookedEventIds.includes(selectedEvent.id) && (
|
||||
<span className="px-2.5 py-1 bg-emerald-50 text-emerald-600 border border-emerald-200 text-[8px] font-black rounded-lg uppercase tracking-wider">
|
||||
@@ -161,36 +283,10 @@ const UpcomingEventsSlider: React.FC<UpcomingEventsSliderProps> = ({
|
||||
</div>
|
||||
|
||||
<div className="mt-4 shrink-0 border-t border-gray-100 pt-6 flex flex-col gap-3">
|
||||
{userRole === 'STUDENT' && onToggleBooking && (
|
||||
<>
|
||||
{bookedEventIds.includes(selectedEvent.id) ? (
|
||||
<button
|
||||
onClick={() => {
|
||||
if (window.confirm(`Are you sure you want to cancel your registration for "${selectedEvent.title}"?`)) {
|
||||
onToggleBooking(selectedEvent.id);
|
||||
setSelectedEvent(null);
|
||||
}
|
||||
}}
|
||||
className="w-full py-4 bg-rose-600 text-white rounded-2xl font-black uppercase text-[10px] tracking-widest hover:bg-rose-700 transition-all shadow-xl shadow-rose-200 focus:outline-none focus:ring-4 focus:ring-rose-100"
|
||||
>
|
||||
Cancel Registration
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => {
|
||||
onToggleBooking(selectedEvent.id);
|
||||
setSelectedEvent(null);
|
||||
}}
|
||||
className="w-full py-4 bg-emerald-600 text-white rounded-2xl font-black uppercase text-[10px] tracking-widest hover:bg-emerald-700 transition-all shadow-xl shadow-emerald-200 focus:outline-none focus:ring-4 focus:ring-emerald-100"
|
||||
>
|
||||
Get Tickets
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() => setSelectedEvent(null)}
|
||||
className="w-full py-4 bg-slate-900 text-white rounded-2xl font-black uppercase text-[10px] tracking-widest hover:bg-black transition-all shadow-xl shadow-slate-200 focus:outline-none focus:ring-4 focus:ring-slate-100"
|
||||
className="w-full py-4 bg-slate-900 text-white rounded-2xl font-black uppercase text-[10px] tracking-widest hover:bg-black transition-all shadow-xl shadow-slate-200 focus:outline-none focus:ring-4 focus:ring-slate-100 cursor-pointer"
|
||||
>
|
||||
Close Summary
|
||||
</button>
|
||||
|
||||
@@ -618,6 +618,9 @@ window.fetch = async function(input: RequestInfo | URL, init?: RequestInit): Pro
|
||||
for (const event of events) {
|
||||
const count = registrations.filter(r => String(r.eventId || r.event_id) === String(event.id)).length;
|
||||
event.currentParticipants = count;
|
||||
if (event["total capacity"] !== undefined) {
|
||||
event.maxParticipants = event["total capacity"];
|
||||
}
|
||||
|
||||
if (!['APPROVED', 'COMPLETED', 'CANCELLED'].includes(event.status)) {
|
||||
const conflictMsg = await getConflictMessage(event);
|
||||
@@ -741,6 +744,7 @@ window.fetch = async function(input: RequestInfo | URL, init?: RequestInit): Pro
|
||||
guestSocialProfile: payload.socialProfile || null,
|
||||
requirements: payload.requirements || [],
|
||||
targetedSections: payload.targetedSections || [],
|
||||
targetDepartments: payload.targetDepartments || [],
|
||||
groupRequestId: payload.groupRequestId || null,
|
||||
sponsors: payload.sponsors || [],
|
||||
budget: budgetStr === "" ? 0.0 : parseFloat(budgetStr),
|
||||
@@ -751,11 +755,14 @@ window.fetch = async function(input: RequestInfo | URL, init?: RequestInit): Pro
|
||||
isPublicEvent: !!payload.isPublicEvent,
|
||||
image: payload.image || null,
|
||||
targetedBatch: payload.targetedBatch || null,
|
||||
"total capacity": payload.maxParticipants === '' || payload.maxParticipants === undefined ? null : Number(payload.maxParticipants),
|
||||
maxParticipants: payload.maxParticipants === '' || payload.maxParticipants === undefined ? null : Number(payload.maxParticipants),
|
||||
proposer: {
|
||||
id: proposer.id,
|
||||
fullName: proposer.fullName,
|
||||
email: proposer.email,
|
||||
role: proposer.role
|
||||
role: proposer.role,
|
||||
department: proposer.department || "N/A"
|
||||
},
|
||||
dayConfigs: payload.dayConfigs || [],
|
||||
deptLimits: payload.deptLimits || {},
|
||||
@@ -995,6 +1002,12 @@ window.fetch = async function(input: RequestInfo | URL, init?: RequestInit): Pro
|
||||
if (payload.institution !== undefined) updatedFields.institution = payload.institution;
|
||||
if (payload.department !== undefined) updatedFields.department = payload.department;
|
||||
if (payload.venue !== undefined) updatedFields.location = payload.venue;
|
||||
if (payload.category !== undefined) updatedFields.category = payload.category;
|
||||
if (payload.maxParticipants !== undefined) {
|
||||
const val = payload.maxParticipants === '' ? null : Number(payload.maxParticipants);
|
||||
updatedFields["total capacity"] = val;
|
||||
updatedFields.maxParticipants = val;
|
||||
}
|
||||
if (payload.guestName !== undefined) updatedFields.guestName = payload.guestName;
|
||||
if (payload.socialProfile !== undefined) updatedFields.guestSocialProfile = payload.socialProfile;
|
||||
if (payload.academicYears !== undefined) updatedFields.academicYears = payload.academicYears;
|
||||
|
||||
Reference in New Issue
Block a user