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

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">
<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 &amp; 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 &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 || '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>

File diff suppressed because it is too large Load Diff

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

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 && (
<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>
);
};

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

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;