Update frontend components and integrate firebase backend

This commit is contained in:
SACHIN
2026-06-25 08:13:09 +05:30
parent 8af314c629
commit 0b1053a4e8
14 changed files with 923 additions and 513 deletions

View File

@@ -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');

View File

@@ -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" />
{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>

View File

@@ -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">
{/* 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="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>
<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" />
Date & Time
Start Date &amp; Time
</div>
<p className="text-sm font-bold text-text-dark">{new Date(selectedEvent.startDate).toLocaleString()}</p>
<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="space-y-1">
<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 &amp; 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}</p>
<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>
<div className="space-y-6">
<div className="space-y-1">
{/* 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" />
Department
Event Scope
</div>
<p className="text-sm font-bold text-text-dark">{selectedEvent.department}</p>
<p className="text-sm font-bold text-text-dark">
{selectedEvent.department === 'Institutional' ? 'Institutional Event' : 'Departmental Event'}
</p>
</div>
<div className="space-y-1">
<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 Batches
Target Audience
</div>
<p className="text-sm font-bold text-text-dark">{selectedEvent.academicYears?.join(', ') || 'N/A'}</p>
<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">
{/* 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>
</div>
)}
{/* 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" />
Budget
Financial Details
</div>
<p className="text-lg font-black text-text-dark">{selectedEvent.budget?.toLocaleString() || '0'}</p>
<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>
<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">
{(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>

View File

@@ -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';
@@ -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[],
@@ -281,13 +298,28 @@ 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' :
eventType: eventScope === 'INSTITUTIONAL' ? '' :
eventScope === 'CLUB' ? 'Club' :
eventScope === 'PLACEMENT' ? 'Placement' : '',
category: eventScope === 'CENTRE' ? 'CENTRE' : (eventScope === 'CLUB' ? 'CLUB' : 'ACADEMIC'),
@@ -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,6 +406,15 @@ 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() &&
@@ -461,8 +498,8 @@ 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)
@@ -470,23 +507,32 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
const minSufficientCapacity = validCapacities.length > 0 ? Math.min(...validCapacities) : totalExpectedStrength;
return venues.filter(v => {
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;
// 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]);
}
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;
@@ -575,19 +615,6 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
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))
)
: [];
// 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 primaryPayload = {
...formData,
startDate: earliestStart.toISOString(),
@@ -601,15 +628,19 @@ 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,
department: formData.department,
venue: formData.venue === 'Others.' ? formData.customVenue : formData.venue,
groupRequestId,
academicYears: batchesToAssign,
targetedSections: deptSections,
targetedSections: formData.targetedSections,
userId: user?.id,
budget: formData.budget === '' ? 0 : Number(formData.budget),
registrationFee: formData.hasRegistrationFee ? Number(formData.registrationFee) : 0,
category: eventScope === 'CLUB' ? 'CLUB' : (eventScope === 'INSTITUTIONAL' ? 'INSTITUTIONAL' : (eventScope === 'CENTRE' ? 'CENTRE' : 'ACADEMIC')),
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,
@@ -632,15 +663,10 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
setIsSubmitting(false);
return;
}
throw new Error(`[${dept}] ${err.message || 'Submission failed'}`);
throw new Error(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(),
@@ -654,11 +680,11 @@ 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,
department: formData.department,
groupRequestId,
eventName: alternateEvent.eventName || `${formData.eventName} (Division 2)`,
venue: alternateEvent.venue === 'Others.' ? alternateEvent.customVenue : alternateEvent.venue,
targetedSections: deptLeftOut,
targetedSections: leftOutSections,
academicYears: batchesToAssign,
userId: user?.id,
budget: 0,
@@ -673,8 +699,6 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
});
}
}
}
}
setSubmitted(true);
} catch (err: any) {
setError(err.message);
@@ -837,13 +861,14 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
</div>
</>
) : eventScope === 'INSTITUTIONAL' ? (
<div className="md:col-span-2 relative group">
<>
<div>
<label className="text-[10px] font-black uppercase tracking-widest text-text-muted mb-3 block">Select Institutional Event</label>
<div className="relative">
<div className="relative group">
<select
required
value={formData.eventName}
onChange={e => setFormData({...formData, eventName: e.target.value, eventType: 'Institutional'})}
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>
@@ -854,6 +879,17 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
<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>
<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>
@@ -867,6 +903,24 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
</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>

View File

@@ -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);

View File

@@ -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}

View File

@@ -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);
}
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">

View File

@@ -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>

View File

@@ -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 && (
<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 rounded-xl mb-4 shrink-0 shadow-sm border border-black/10"
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>
);
};

View File

@@ -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>

View File

@@ -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)',
}}
/>

View File

@@ -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

View File

@@ -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}
{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 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>

View File

@@ -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;