Convert backends to Firebase and combine projects
This commit is contained in:
35
RIT-EVENT-MANAGEMENT--main/.gitignore
vendored
Normal file
35
RIT-EVENT-MANAGEMENT--main/.gitignore
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# SQL scripts (one-time migrations)
|
||||
*.sql
|
||||
|
||||
# OS files
|
||||
Thumbs.db
|
||||
826
RIT-EVENT-MANAGEMENT--main/App.tsx
Normal file
826
RIT-EVENT-MANAGEMENT--main/App.tsx
Normal file
@@ -0,0 +1,826 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import WelcomeScreen from './components/WelcomeScreen';
|
||||
import LoginForm from './components/LoginForm';
|
||||
import Dashboard from './components/Dashboard';
|
||||
import AdminLandingPage from './components/AdminLandingPage';
|
||||
import FacultyDashboard from './components/FacultyDashboard';
|
||||
import TicketVerificationView from './components/TicketVerificationView';
|
||||
import { AppState, UserRole, Event, EventSchedule, Announcement, Ticket } from './types';
|
||||
import { supabase } from './supabase';
|
||||
|
||||
const App: React.FC = () => {
|
||||
const [appState, setAppState] = useState<AppState>('WELCOME');
|
||||
const [userRole, setUserRole] = useState<UserRole | null>(null);
|
||||
const [events, setEvents] = useState<Event[]>([]);
|
||||
const [announcements, setAnnouncements] = useState<Announcement[]>([]);
|
||||
const [specialEvents, setSpecialEvents] = useState<any[]>([]);
|
||||
const [domains, setDomains] = useState<any[]>([]);
|
||||
const [userRegistrations, setUserRegistrations] = useState<any[]>([]);
|
||||
const [allRegistrations, setAllRegistrations] = useState<any[]>([]);
|
||||
const [bookedEventIds, setBookedEventIds] = useState<string[]>([]);
|
||||
const [currentUserName, setCurrentUserName] = useState<string>('User');
|
||||
const [currentUserDept, setCurrentUserDept] = useState<string>('');
|
||||
const [currentUserEmail, setCurrentUserEmail] = useState<string>('');
|
||||
const [currentUserPhone, setCurrentUserPhone] = useState<string>('');
|
||||
const [currentUserPhoto, setCurrentUserPhoto] = useState<string>('');
|
||||
const [currentUserId, setCurrentUserId] = useState<string>('');
|
||||
const [currentUserFacultyRole, setCurrentUserFacultyRole] = useState<string>('');
|
||||
const [ticketToVerify, setTicketToVerify] = useState<{ ticket: Ticket, event: Event } | null>(null);
|
||||
const [isAuthenticating, setIsAuthenticating] = useState(false);
|
||||
const [globalError, setGlobalError] = useState<string | null>(null);
|
||||
const [studentDocuments, setStudentDocuments] = useState<Record<string, string>>({});
|
||||
const [showStudentHubForAdmin, setShowStudentHubForAdmin] = useState(false);
|
||||
const [intendedRole, setIntendedRole] = useState<UserRole | null>(null);
|
||||
|
||||
const mapDbToEvent = useCallback((dbEvent: any, registrations: any[] = []): Event => {
|
||||
const eventRegs = registrations.filter(r => String(r.event_id) === String(dbEvent.id));
|
||||
const deptCounts: Record<string, number> = {};
|
||||
const deptSectionCounts: Record<string, Record<string, number>> = {};
|
||||
|
||||
eventRegs.forEach(reg => {
|
||||
if (reg.dept) {
|
||||
deptCounts[reg.dept] = (deptCounts[reg.dept] || 0) + 1;
|
||||
if (reg.section) {
|
||||
if (!deptSectionCounts[reg.dept]) {
|
||||
deptSectionCounts[reg.dept] = {};
|
||||
}
|
||||
deptSectionCounts[reg.dept][reg.section] = (deptSectionCounts[reg.dept][reg.section] || 0) + 1;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
id: String(dbEvent.id),
|
||||
title: dbEvent.title,
|
||||
location: dbEvent.location,
|
||||
date: dbEvent.date,
|
||||
category: dbEvent.category,
|
||||
domain: dbEvent.domain,
|
||||
pricingType: dbEvent.pricing_type,
|
||||
coordinator: dbEvent.coordinator,
|
||||
image: dbEvent.image,
|
||||
status: dbEvent.status,
|
||||
maxParticipants: dbEvent.max_participants,
|
||||
registrationDeadline: dbEvent.registration_deadline,
|
||||
durationDays: dbEvent.duration_days,
|
||||
club: dbEvent.club,
|
||||
deptLimits: dbEvent.deptLimits || {},
|
||||
deptSectionLimits: dbEvent.deptSectionLimits || {},
|
||||
event_summary: dbEvent.event_summary,
|
||||
isTeamEvent: dbEvent.is_team_event,
|
||||
teamSizeLimit: dbEvent.team_size_limit,
|
||||
teamComposition: dbEvent.team_composition,
|
||||
currentParticipants: eventRegs.length,
|
||||
currentDeptCounts: deptCounts,
|
||||
currentDeptSectionCounts: deptSectionCounts,
|
||||
created_by: dbEvent.created_by,
|
||||
participantType: dbEvent.participant_type as any,
|
||||
verificationStatus: dbEvent.verification_status as any,
|
||||
refreshment_expense: dbEvent.refreshment_expense ?? 0,
|
||||
transportation_expense: dbEvent.transportation_expense ?? 0,
|
||||
session_coverage_fee: dbEvent.session_coverage_fee ?? 0,
|
||||
total_expense: dbEvent.total_expense ?? 0,
|
||||
conducting_dept: dbEvent.conducting_dept ?? null,
|
||||
request_by_faculty: dbEvent.request_by_faculty ?? null,
|
||||
request_by_HOD: dbEvent.request_by_hod ?? null,
|
||||
};
|
||||
}, []);
|
||||
|
||||
const fetchAnnouncements = useCallback(async () => {
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('announcements')
|
||||
.select('*')
|
||||
.order('timestamp', { ascending: false });
|
||||
|
||||
if (data) {
|
||||
setAnnouncements(data.map(ann => ({
|
||||
id: String(ann.id),
|
||||
title: ann.title,
|
||||
message: ann.message,
|
||||
type: ann.type,
|
||||
eventId: ann.event_id ? String(ann.event_id) : undefined,
|
||||
timestamp: ann.timestamp || new Date().toISOString(),
|
||||
expiresAt: ann.expires_at
|
||||
})));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Announcement Sync Error:", err);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const fetchSpecialEvents = useCallback(async () => {
|
||||
try {
|
||||
let query = supabase.from('special_events').select('*').eq('is_active', true);
|
||||
|
||||
// SECURITY: Public users and Students only see Approved special events
|
||||
if (!userRole || userRole === 'STUDENT') {
|
||||
query = query.eq('verification_status', 'APPROVED');
|
||||
}
|
||||
|
||||
const { data } = await query.order('created_at', { ascending: false });
|
||||
if (data) {
|
||||
setSpecialEvents(data.map((se: any) => ({
|
||||
...se,
|
||||
verificationStatus: se.verification_status
|
||||
})));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Special Events Sync Error:", err);
|
||||
}
|
||||
}, [userRole]);
|
||||
|
||||
const fetchDomains = useCallback(async () => {
|
||||
try {
|
||||
const { data } = await supabase.from('domains').select('*').order('name', { ascending: true });
|
||||
if (data) setDomains(data);
|
||||
} catch (err) {
|
||||
console.error("Domains Sync Error:", err);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const fetchEvents = useCallback(async (registrations: any[] = []) => {
|
||||
const { data } = await supabase.from('events').select('*').order('date', { ascending: true });
|
||||
if (data) {
|
||||
// Fetch all schedule + resource_persons + dept limits in bulk
|
||||
const { data: scheduleData } = await supabase.from('event_schedule').select('*').order('day_idx', { ascending: true }).order('batch_idx', { ascending: true });
|
||||
const { data: rpData } = await supabase.from('resource_persons').select('*');
|
||||
const { data: deptLimitsData } = await supabase.from('event_dept_limits').select('*');
|
||||
|
||||
const mapped = data.map(e => {
|
||||
const event = mapDbToEvent(e, registrations);
|
||||
|
||||
// Attach schedule rows for this event
|
||||
const eventSchedule: EventSchedule[] = (scheduleData || []).filter(s => s.event_id === e.id).map(s => {
|
||||
const rp = (rpData || []).find(r => r.schedule_id === s.id);
|
||||
return {
|
||||
...s,
|
||||
resource_person: rp ? { id: rp.id, type: rp.type, name: rp.name, dept: rp.dept, college_name: rp.college_name, phone: rp.phone, email: rp.email } : undefined,
|
||||
};
|
||||
});
|
||||
event.schedule = eventSchedule;
|
||||
|
||||
// Attach department limits for this event
|
||||
const eventDeptLimits: Record<string, number> = {};
|
||||
const eventDeptSectionLimits: Record<string, Record<string, number>> = {};
|
||||
(deptLimitsData || []).filter(dl => dl.event_id === e.id).forEach(dl => {
|
||||
eventDeptLimits[dl.department] = dl.max_seats;
|
||||
eventDeptSectionLimits[dl.department] = dl.section_limits || {};
|
||||
});
|
||||
event.deptLimits = eventDeptLimits;
|
||||
event.deptSectionLimits = eventDeptSectionLimits;
|
||||
|
||||
return event;
|
||||
});
|
||||
// SECURITY: Public users and Students only see Approved events
|
||||
if (!userRole || userRole === 'STUDENT') {
|
||||
setEvents(mapped.filter(e => e.verificationStatus === 'APPROVED'));
|
||||
} else {
|
||||
setEvents(mapped);
|
||||
}
|
||||
}
|
||||
}, [mapDbToEvent, userRole]);
|
||||
|
||||
const fetchAllRegistrations = useCallback(async () => {
|
||||
const { data } = await supabase.from('registrations').select('*').order('registered_at', { ascending: false });
|
||||
if (data) {
|
||||
setAllRegistrations(data);
|
||||
fetchEvents(data);
|
||||
}
|
||||
}, [fetchEvents]);
|
||||
|
||||
const fetchMyRegs = useCallback(async (userId: string) => {
|
||||
const { data } = await supabase.from('registrations').select('*').eq('user_id', userId);
|
||||
if (data) {
|
||||
setUserRegistrations(data);
|
||||
setBookedEventIds(data.map((r: any) => String(r.event_id)));
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleAuthChange = useCallback(async (session: any) => {
|
||||
if (!session?.user) {
|
||||
setAppState(prev => {
|
||||
if (prev !== 'WELCOME' && prev !== 'LOGIN') return 'WELCOME';
|
||||
return prev;
|
||||
});
|
||||
setIsAuthenticating(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsAuthenticating(true);
|
||||
const user = session.user;
|
||||
|
||||
const HIGH_AUTH_ADMINS = [
|
||||
'adrit1highauth@gmail.com',
|
||||
'adrit2highauth@gmail.com',
|
||||
'adrit3highauth@gmail.com',
|
||||
'adrit4highauth@gmail.com',
|
||||
'adrit5highauth@gmail.com'
|
||||
];
|
||||
|
||||
try {
|
||||
let profile = null;
|
||||
let finalRole: UserRole | null = null;
|
||||
|
||||
// Check for fixed high-auth admins first
|
||||
if (user.email && HIGH_AUTH_ADMINS.includes(user.email)) {
|
||||
const { data: admin } = await supabase.from('Adminusers').select('*').eq('id', user.id).single();
|
||||
if (admin) {
|
||||
profile = admin;
|
||||
} else {
|
||||
// If they exist in auth but not in Adminusers yet, we create and insert a profile
|
||||
const newProfile = {
|
||||
id: user.id,
|
||||
name: 'System Admin',
|
||||
email: user.email,
|
||||
dept: 'Administration',
|
||||
updated_at: new Date().toISOString()
|
||||
};
|
||||
const { data: inserted, error: insertError } = await supabase.from('Adminusers').insert(newProfile).select().single();
|
||||
if (!insertError) {
|
||||
profile = inserted;
|
||||
} else {
|
||||
profile = newProfile; // Fallback to local object if insert fails
|
||||
}
|
||||
}
|
||||
finalRole = 'ADMIN'; // Principal/Director is ADMIN
|
||||
} else {
|
||||
// Check Adminusers (Admin Hub / System Admins)
|
||||
const { data: adminProfile } = await supabase.from('Adminusers').select('*').eq('id', user.id).single();
|
||||
if (adminProfile) {
|
||||
profile = adminProfile;
|
||||
finalRole = 'ADMIN';
|
||||
} else {
|
||||
// Check Facultyusers (Event Coordinator Hub)
|
||||
const { data: facultyProfile } = await supabase.from('Facultyusers').select('*').eq('id', user.id).single();
|
||||
if (facultyProfile) {
|
||||
profile = facultyProfile;
|
||||
finalRole = 'COORDINATOR';
|
||||
} else {
|
||||
const { data: student } = await supabase.from('Studentusers').select('*').eq('id', user.id).single();
|
||||
if (student) {
|
||||
profile = student;
|
||||
finalRole = 'STUDENT';
|
||||
} else {
|
||||
const { data: externalStudent } = await supabase.from('externalusers').select('*').eq('id', user.id).single();
|
||||
if (externalStudent) {
|
||||
profile = externalStudent;
|
||||
finalRole = 'STUDENT';
|
||||
} else {
|
||||
// FALLBACK: Auto-sync profile from metadata
|
||||
const meta = user.user_metadata;
|
||||
// ... (rest of metadata sync logic)
|
||||
if (meta && (meta.role === 'STUDENT' || meta.regNo)) {
|
||||
const isExternal = meta.signUpType === 'EXTERNAL';
|
||||
const table = isExternal ? 'externalusers' : 'Studentusers';
|
||||
|
||||
const syncData: any = {
|
||||
id: user.id,
|
||||
name: meta.name || user.email?.split('@')[0],
|
||||
email: user.email,
|
||||
reg_no: meta.regNo || meta.reg_no,
|
||||
phone: meta.phone,
|
||||
department: meta.department || meta.dept,
|
||||
year: meta.year,
|
||||
section: meta.section,
|
||||
updated_at: new Date().toISOString()
|
||||
};
|
||||
|
||||
if (isExternal) {
|
||||
syncData.college = meta.collegeName || meta.college;
|
||||
syncData.college_location = meta.collegeLocation;
|
||||
syncData.gender = meta.gender;
|
||||
}
|
||||
|
||||
const { data: synced, error: syncError } = await supabase
|
||||
.from(table)
|
||||
.upsert(syncData)
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (!syncError && synced) {
|
||||
profile = synced;
|
||||
finalRole = 'STUDENT';
|
||||
} else {
|
||||
console.warn("Auto-sync profile failed:", syncError?.message);
|
||||
profile = syncData; // Use metadata-based object as semi-functional fallback
|
||||
finalRole = 'STUDENT';
|
||||
}
|
||||
} else if (meta && (meta.role === 'ADMIN' || meta.role === 'COORDINATOR')) {
|
||||
// If metadata says Admin/Coordinator but they weren't in tables, check HIGH_AUTH_ADMINS
|
||||
if (user.email && HIGH_AUTH_ADMINS.includes(user.email)) {
|
||||
finalRole = 'ADMIN';
|
||||
} else {
|
||||
finalRole = 'COORDINATOR';
|
||||
}
|
||||
|
||||
// Try to find in Facultyusers or Adminusers as fallback
|
||||
const { data: f } = await supabase.from('Facultyusers').select('*').eq('id', user.id).single();
|
||||
if (f) profile = f;
|
||||
else {
|
||||
const { data: a } = await supabase.from('Adminusers').select('*').eq('id', user.id).single();
|
||||
if (a) profile = a;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (profile && finalRole) {
|
||||
// PORTAL SECURITY GUARD: Ensure actual role matches the intended portal
|
||||
if (intendedRole && finalRole !== intendedRole) {
|
||||
// Exception: ADMINs can access COORDINATOR portal if needed, but not vice-versa
|
||||
const isPermittedOverload = (intendedRole === 'COORDINATOR' && finalRole === 'ADMIN');
|
||||
|
||||
if (!isPermittedOverload) {
|
||||
setGlobalError(`Access Denied: Your account (${finalRole}) is not authorized for the ${intendedRole} Portal.`);
|
||||
await supabase.auth.signOut();
|
||||
setIsAuthenticating(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setCurrentUserName(profile.name || 'User');
|
||||
setCurrentUserDept(profile.department || profile.dept || '');
|
||||
setCurrentUserEmail(profile.email || user.email || '');
|
||||
setCurrentUserPhone(profile.phone || '');
|
||||
setCurrentUserPhoto(profile.profile_photo || '');
|
||||
setCurrentUserId(user.id);
|
||||
setCurrentUserFacultyRole(profile.role || '');
|
||||
setUserRole(finalRole);
|
||||
|
||||
// Only redirect if we are in a state that warrants it (e.g. just logged in)
|
||||
setAppState(prev => {
|
||||
if (prev === 'WELCOME' || prev === 'LOGIN' || prev === 'VERIFY') {
|
||||
if (finalRole === 'ADMIN') {
|
||||
if (intendedRole === 'COORDINATOR') {
|
||||
return 'ADMIN_LANDING'; // Event Co Hub for Admins who chose it
|
||||
}
|
||||
return 'FACULTY_DASHBOARD'; // Default Admin Hub
|
||||
}
|
||||
if (finalRole === 'COORDINATOR') return 'ADMIN_LANDING';
|
||||
return 'DASHBOARD';
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
|
||||
// Fetch data but don't let it block the UI if it takes too long
|
||||
Promise.all([
|
||||
fetchAllRegistrations(),
|
||||
fetchMyRegs(user.id),
|
||||
fetchAnnouncements(),
|
||||
fetchSpecialEvents(),
|
||||
fetchDomains()
|
||||
]).catch(err => console.error("Post-auth fetch error:", err));
|
||||
|
||||
} else {
|
||||
setGlobalError("Access Denied: Account structure invalid.");
|
||||
await supabase.auth.signOut();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Auth routing error:", err);
|
||||
} finally {
|
||||
setIsAuthenticating(false);
|
||||
}
|
||||
}, [fetchAllRegistrations, fetchMyRegs, fetchAnnouncements, fetchSpecialEvents, fetchDomains, intendedRole]);
|
||||
|
||||
useEffect(() => {
|
||||
// Initial public data fetch
|
||||
fetchAllRegistrations();
|
||||
fetchAnnouncements();
|
||||
fetchSpecialEvents();
|
||||
fetchDomains();
|
||||
|
||||
// 1. Initial Session Recovery & Error Handling
|
||||
const initAuth = async () => {
|
||||
try {
|
||||
const { data: { session }, error } = await supabase.auth.getSession();
|
||||
if (error) {
|
||||
console.warn("[Auth System] Session recovery failed:", error.message);
|
||||
// Broaden the error detection for refresh token issues
|
||||
const isRefreshTokenError =
|
||||
error.message.includes('Refresh Token') ||
|
||||
error.message.includes('not found') ||
|
||||
error.message.includes('Invalid Refresh Token') ||
|
||||
error.status === 400 ||
|
||||
error.status === 401;
|
||||
|
||||
if (isRefreshTokenError) {
|
||||
console.log("[Auth System] Stale or invalid session detected. Clearing storage...");
|
||||
// Explicitly clear the storage key used in supabase.ts
|
||||
localStorage.removeItem('rit-events-hub-auth');
|
||||
// Attempt a clean sign out
|
||||
await supabase.auth.signOut().catch(() => {});
|
||||
setAppState('WELCOME');
|
||||
}
|
||||
}
|
||||
if (session) {
|
||||
handleAuthChange(session);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("[Auth System] Critical recovery error:", err);
|
||||
}
|
||||
};
|
||||
|
||||
initAuth();
|
||||
|
||||
// 2. Real-time Auth State Subscription
|
||||
const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => {
|
||||
handleAuthChange(session);
|
||||
});
|
||||
return () => subscription.unsubscribe();
|
||||
}, [handleAuthChange]);
|
||||
|
||||
// NOTE: CreateEventForm already handles the Supabase insert.
|
||||
// This callback only refreshes the local events list.
|
||||
const handleAddEvent = async (_ev: Event) => {
|
||||
await fetchAllRegistrations();
|
||||
};
|
||||
|
||||
const handleDeleteEvent = async (id: string) => {
|
||||
// Step 1: Delete all registrations for this event first (FK constraint: NO ACTION)
|
||||
const { error: regError } = await supabase
|
||||
.from('registrations')
|
||||
.delete()
|
||||
.eq('event_id', id);
|
||||
if (regError) {
|
||||
console.error('[Delete Event] Failed to delete registrations:', regError);
|
||||
}
|
||||
// Step 2: Delete any announcements linked to this event
|
||||
await supabase.from('announcements').delete().eq('event_id', id);
|
||||
// Step 3: Now delete the event itself
|
||||
const { error } = await supabase.from('events').delete().eq('id', id);
|
||||
if (error) {
|
||||
console.error('[Delete Event] Supabase error:', error);
|
||||
alert(`Failed to delete event: ${error.message}`);
|
||||
} else {
|
||||
await fetchAllRegistrations(); // Refresh so all hubs update
|
||||
}
|
||||
};
|
||||
|
||||
// NOTE: CreateEventForm handles event_schedule + resource_persons writes.
|
||||
// This only updates the events table columns.
|
||||
const handleUpdateEvent = async (ev: Event) => {
|
||||
const { error } = await supabase.from('events').update({
|
||||
title: ev.title,
|
||||
location: ev.location,
|
||||
date: ev.date,
|
||||
category: ev.category,
|
||||
domain: ev.domain,
|
||||
pricing_type: ev.pricingType,
|
||||
coordinator: ev.coordinator,
|
||||
image: ev.image,
|
||||
status: ev.status,
|
||||
max_participants: ev.maxParticipants,
|
||||
registration_deadline: ev.registrationDeadline,
|
||||
duration_days: ev.durationDays,
|
||||
club: ev.club,
|
||||
event_summary: ev.event_summary,
|
||||
is_team_event: ev.isTeamEvent,
|
||||
team_size_limit: ev.teamSizeLimit,
|
||||
team_composition: ev.teamComposition,
|
||||
participant_type: ev.participantType,
|
||||
verification_status: ev.verificationStatus,
|
||||
refreshment_expense: ev.refreshment_expense || 0,
|
||||
transportation_expense: ev.transportation_expense || 0,
|
||||
session_coverage_fee: (ev as any).session_coverage_fee || 0,
|
||||
total_expense: ev.total_expense || 0,
|
||||
request_by_faculty: ev.request_by_faculty || null,
|
||||
request_by_hod: ev.request_by_HOD || null
|
||||
}).eq('id', ev.id);
|
||||
|
||||
if (error) {
|
||||
console.error('[Update Event] Supabase error:', error);
|
||||
alert(`Failed to update event: ${error.message}`);
|
||||
} else {
|
||||
await fetchAllRegistrations();
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateSpecialEvent = async (se: any) => {
|
||||
const { error } = await supabase.from('special_events').update({
|
||||
title: se.title,
|
||||
description: se.description,
|
||||
link: se.link,
|
||||
is_active: se.is_active,
|
||||
verification_status: se.verificationStatus
|
||||
}).eq('id', se.id);
|
||||
|
||||
if (error) {
|
||||
console.error('[Update Special Event] Supabase error:', error);
|
||||
alert(`Failed to update special event: ${error.message}`);
|
||||
} else {
|
||||
await fetchSpecialEvents();
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddAnnouncement = async (ann: Announcement) => {
|
||||
const { error } = await supabase.from('announcements').insert({
|
||||
title: ann.title,
|
||||
message: ann.message,
|
||||
type: ann.type,
|
||||
event_id: ann.eventId,
|
||||
expires_at: ann.expiresAt,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
if (!error) {
|
||||
if (ann.type === 'ONGOING' && ann.eventId) {
|
||||
// Sync participants table when event starts
|
||||
const { data: regs } = await supabase
|
||||
.from('registrations')
|
||||
.select('*')
|
||||
.eq('event_id', ann.eventId)
|
||||
.eq('payment_status', 'COMPLETED');
|
||||
|
||||
if (regs && regs.length > 0) {
|
||||
const participantData = regs.map(r => ({
|
||||
user_id: r.user_id,
|
||||
event_id: r.event_id,
|
||||
user_name: r.user_name,
|
||||
reg_no: r.reg_no,
|
||||
dept: r.dept,
|
||||
year: r.year,
|
||||
section: r.section,
|
||||
participation_date: new Date().toISOString()
|
||||
}));
|
||||
|
||||
await supabase.from('participants').upsert(participantData);
|
||||
}
|
||||
}
|
||||
await fetchAnnouncements();
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateAnnouncement = async (ann: Announcement) => {
|
||||
const { error } = await supabase.from('announcements')
|
||||
.update({
|
||||
title: ann.title,
|
||||
message: ann.message,
|
||||
type: ann.type,
|
||||
event_id: ann.eventId,
|
||||
expires_at: ann.expiresAt
|
||||
})
|
||||
.eq('id', ann.id);
|
||||
|
||||
if (!error) {
|
||||
await fetchAnnouncements();
|
||||
} else {
|
||||
console.error("[Database Hub] Update Rejection:", error);
|
||||
alert(`Update failed: ${error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteAnnouncement = async (id: string) => {
|
||||
// 1. Retrieve notice details before deletion to check type
|
||||
const { data: targetAnn } = await supabase.from('announcements').select('*').eq('id', id).single();
|
||||
|
||||
const { error } = await supabase.from('announcements')
|
||||
.delete()
|
||||
.eq('id', id);
|
||||
|
||||
if (!error) {
|
||||
// 2. If it was an 'ENDED' or 'ONGOING' notice, revert event status and wipe records
|
||||
if (targetAnn && (targetAnn.type === 'ENDED' || targetAnn.type === 'ONGOING') && targetAnn.event_id) {
|
||||
await supabase.from('events').update({ status: 'Scheduled' }).eq('id', targetAnn.event_id);
|
||||
|
||||
// Wipe attendance and participants for a clean restart
|
||||
await supabase.from('attendance_records').delete().eq('event_id', targetAnn.event_id);
|
||||
await supabase.from('participants').delete().eq('event_id', targetAnn.event_id);
|
||||
|
||||
await fetchAllRegistrations(); // Sync the event list
|
||||
}
|
||||
|
||||
setAnnouncements(prev => prev.filter(ann => ann.id !== id));
|
||||
await fetchAnnouncements();
|
||||
} else {
|
||||
console.error("[Database Hub] Delete Rejection:", error);
|
||||
alert(`Delete rejected: ${error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleBooking = async (eventId: string) => {
|
||||
if (userRole !== 'STUDENT') {
|
||||
alert("Event Coordinators and Admins cannot register for events.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
if (!user) {
|
||||
alert("You must be logged in to register.");
|
||||
return;
|
||||
}
|
||||
|
||||
const registrationId = `${user.id}_${eventId}`;
|
||||
if (bookedEventIds.includes(eventId)) {
|
||||
const { error } = await supabase.from('registrations').delete().eq('id', registrationId);
|
||||
if (error) throw error;
|
||||
|
||||
setBookedEventIds(prev => prev.filter(id => id !== eventId));
|
||||
await fetchAllRegistrations();
|
||||
} else {
|
||||
// Block registration if there is any unfinalized completed event (missing certificate upload or missing OD approval)
|
||||
const unfinalized = userRegistrations.find(reg => {
|
||||
const ev = events.find(e => String(e.id) === String(reg.event_id));
|
||||
if (ev && ev.status === 'Completed') {
|
||||
const hasUploadedCert = !!reg.certification_url;
|
||||
const hasOd = !!(reg.od_url || reg.od);
|
||||
return !hasUploadedCert || !hasOd;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (unfinalized) {
|
||||
const ev = events.find(e => String(e.id) === String(unfinalized.event_id));
|
||||
alert(`Registration Blocked: You must upload your certificate and obtain OD approval for your completed event "${ev?.title || 'Past Event'}" before you can register for another event.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const targetEvent = events.find(e => e.id === eventId);
|
||||
if (!targetEvent) {
|
||||
alert("Event not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to fetch from Studentusers first, then externalusers
|
||||
let student = null;
|
||||
const { data: internalStudent } = await supabase.from('Studentusers').select('*').eq('id', user.id).single();
|
||||
if (internalStudent) {
|
||||
student = internalStudent;
|
||||
} else {
|
||||
const { data: externalStudent } = await supabase.from('externalusers').select('*').eq('id', user.id).single();
|
||||
if (externalStudent) student = externalStudent;
|
||||
}
|
||||
|
||||
if (!student) {
|
||||
console.error("Student profile not found for user:", user.id);
|
||||
alert("Your student profile was not found. Please contact support.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate overall registration capacity
|
||||
if (targetEvent.maxParticipants && (targetEvent.currentParticipants || 0) >= targetEvent.maxParticipants) {
|
||||
alert("This event has reached its maximum registration limit.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate department/section quota limits (Internal only, external has no sections)
|
||||
if (student.department && targetEvent.deptLimits?.[student.department]) {
|
||||
const sectionLimits = targetEvent.deptSectionLimits?.[student.department];
|
||||
const hasSectionLimits = sectionLimits && Object.keys(sectionLimits).length > 0;
|
||||
|
||||
if (hasSectionLimits) {
|
||||
const studentSection = student.section || '';
|
||||
const limit = sectionLimits[studentSection];
|
||||
if (!limit || limit <= 0) {
|
||||
alert(`Registration Blocked: Section ${studentSection} of ${student.department} department is not allowed to register for this event.`);
|
||||
return;
|
||||
}
|
||||
const currentSectionCount = targetEvent.currentDeptSectionCounts?.[student.department]?.[studentSection] || 0;
|
||||
if (currentSectionCount >= limit) {
|
||||
alert(`Registration Blocked: The quota for Section ${studentSection} of department ${student.department} is full.`);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
const currentDeptCount = targetEvent.currentDeptCounts?.[student.department] || 0;
|
||||
if (currentDeptCount >= targetEvent.deptLimits[student.department]) {
|
||||
alert(`Registration Blocked: The quota for department ${student.department} is full.`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { error } = await supabase.from('registrations').insert({
|
||||
id: registrationId,
|
||||
user_id: user.id,
|
||||
event_id: eventId,
|
||||
user_email: user.email,
|
||||
user_name: student.name,
|
||||
reg_no: student.reg_no,
|
||||
phone: student.phone,
|
||||
gender: student.gender,
|
||||
dept: student.department,
|
||||
section: student.section,
|
||||
year: student.year,
|
||||
college: student.college_name || student.college || 'Rajalakshmi Institute of Technology',
|
||||
payment_status: targetEvent?.pricingType === 'FREE' ? 'COMPLETED' : 'PENDING'
|
||||
});
|
||||
|
||||
if (error) {
|
||||
if (error.code === '23505') {
|
||||
alert("You are already registered for this event.");
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
} else {
|
||||
await fetchMyRegs(user.id);
|
||||
await fetchAllRegistrations();
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error("Booking Error:", err);
|
||||
alert(`Registration failed: ${err.message || 'Unknown error'}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogout = async () => {
|
||||
await supabase.auth.signOut();
|
||||
setAppState('WELCOME');
|
||||
setUserRole(null);
|
||||
setCurrentUserId('');
|
||||
setCurrentUserFacultyRole('');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen relative">
|
||||
{isAuthenticating && (
|
||||
<div className="fixed inset-0 z-[9999] bg-white/95 backdrop-blur-xl flex flex-col items-center justify-center animate-in fade-in duration-300">
|
||||
<div className="w-16 h-16 border-4 border-slate-200 border-t-[#f97316] rounded-full animate-spin mb-6 shadow-2xl"></div>
|
||||
<h2 className="text-xl font-black text-slate-800 uppercase tracking-widest">Verifying Portal</h2>
|
||||
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-[0.4em] mt-2">Authenticating Academic Credentials</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{globalError && (
|
||||
<div className="fixed top-10 left-1/2 -translate-x-1/2 z-[10000] bg-rose-600 text-white px-8 py-4 rounded-2xl shadow-2xl font-black text-xs uppercase tracking-widest flex items-center gap-3 animate-in slide-in-from-top-10">
|
||||
<i className="fas fa-exclamation-triangle"></i>
|
||||
{globalError}
|
||||
<button onClick={() => setGlobalError(null)} className="ml-4 hover:opacity-50"><i className="fas fa-times"></i></button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{appState === 'WELCOME' && <WelcomeScreen onSelectRole={(role) => { setIntendedRole(role); setUserRole(role); setAppState('LOGIN'); }} events={events} />}
|
||||
{appState === 'LOGIN' && userRole && (
|
||||
<LoginForm
|
||||
role={userRole}
|
||||
onSuccess={() => setIsAuthenticating(true)}
|
||||
onBack={() => setAppState('WELCOME')}
|
||||
/>
|
||||
)}
|
||||
{appState === 'DASHBOARD' && (userRole === 'STUDENT' || userRole === 'COORDINATOR' || userRole === 'ADMIN') && (
|
||||
<Dashboard
|
||||
userRole={userRole}
|
||||
events={events}
|
||||
announcements={announcements}
|
||||
bookedEventIds={bookedEventIds}
|
||||
userRegistrations={userRegistrations}
|
||||
onToggleBooking={toggleBooking}
|
||||
onLogout={handleLogout}
|
||||
currentUserName={currentUserName}
|
||||
onUploadCertificate={async () => ""}
|
||||
studentDocuments={studentDocuments}
|
||||
onUploadDoc={(studentId, data) => setStudentDocuments(prev => ({ ...prev, [studentId]: data }))}
|
||||
onBackToCoordinatorHub={(userRole === 'COORDINATOR' || userRole === 'ADMIN') ? () => setAppState('ADMIN_LANDING') : undefined}
|
||||
specialEvents={specialEvents}
|
||||
/>
|
||||
)}
|
||||
{appState === 'ADMIN_LANDING' && (userRole === 'COORDINATOR' || userRole === 'ADMIN') && (
|
||||
<AdminLandingPage
|
||||
onBack={userRole === 'ADMIN' ? () => setAppState('FACULTY_DASHBOARD') : handleLogout}
|
||||
onAddEvent={handleAddEvent}
|
||||
onUpdateEvent={handleUpdateEvent}
|
||||
onDeleteEvent={handleDeleteEvent}
|
||||
events={events} announcements={announcements}
|
||||
onAddAnnouncement={handleAddAnnouncement}
|
||||
onDeleteAnnouncement={handleDeleteAnnouncement}
|
||||
onUpdateAnnouncement={handleUpdateAnnouncement}
|
||||
studentDocuments={studentDocuments} onApproveCertificate={async (id) => { await supabase.from('registrations').update({ certification_status: 'APPROVED' }).eq('id', id); fetchAllRegistrations(); }}
|
||||
localRegistrations={allRegistrations}
|
||||
onViewStudentHub={() => setAppState('DASHBOARD')}
|
||||
currentUserDept={currentUserDept}
|
||||
currentUserEmail={currentUserEmail}
|
||||
currentUserName={currentUserName}
|
||||
currentUserPhone={currentUserPhone}
|
||||
currentUserPhoto={currentUserPhoto}
|
||||
currentUserId={currentUserId}
|
||||
currentUserFacultyRole={currentUserFacultyRole}
|
||||
domains={domains}
|
||||
/>
|
||||
)}
|
||||
{appState === 'FACULTY_DASHBOARD' && userRole === 'ADMIN' && (
|
||||
<FacultyDashboard
|
||||
onLogout={handleLogout}
|
||||
events={events}
|
||||
specialEvents={specialEvents}
|
||||
domains={domains}
|
||||
announcements={announcements}
|
||||
studentDocuments={studentDocuments}
|
||||
onApproveCertificate={async (id) => { await supabase.from('registrations').update({ certification_status: 'APPROVED' }).eq('id', id); fetchAllRegistrations(); }}
|
||||
onUpdateEvent={handleUpdateEvent}
|
||||
onUpdateSpecialEvent={handleUpdateSpecialEvent}
|
||||
localRegistrations={allRegistrations}
|
||||
onViewCoordinatorHub={() => setAppState('ADMIN_LANDING')}
|
||||
onViewStudentHub={() => setAppState('DASHBOARD')}
|
||||
currentUserName={currentUserName}
|
||||
currentUserDept={currentUserDept}
|
||||
currentUserEmail={currentUserEmail}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
358
RIT-EVENT-MANAGEMENT--main/EMS_DOCUMENTATION.md
Normal file
358
RIT-EVENT-MANAGEMENT--main/EMS_DOCUMENTATION.md
Normal file
@@ -0,0 +1,358 @@
|
||||
# Rajalakshmi Institute of Technology (RIT) Events Hub — System Documentation
|
||||
|
||||
Welcome to the comprehensive system documentation for the **RIT Events Hub**, a premium, high-fidelity academic event management, tracking, and auditing portal designed for Rajalakshmi Institute of Technology.
|
||||
|
||||
This document provides a complete breakdown of the website's architecture, user roles, file structures, database schema, operational workflows, and features, accompanied by a detailed site map.
|
||||
|
||||
---
|
||||
|
||||
## 1. Overview & Core Mission
|
||||
|
||||
The **RIT Events Hub** is an institutional web application designed to digitize and manage the entire lifecycle of college events. It replaces manual event scheduling, paper registrations, physical ticket checks, and fragmented certificate auditing with a unified, secure portal.
|
||||
|
||||
### Key Objectives:
|
||||
* **Decentralized Event Creation:** Enable faculty members to coordinate, schedule, and estimate budgets for departmental activities.
|
||||
* **Hierarchical Approvals:** Ensure events undergo appropriate institutional oversight through a HOD-to-Admin approval pipeline.
|
||||
* **Intelligent Resource Management:** Prevent venue conflicts and respect departmental/sectional seat quotas.
|
||||
* **Continuous Verification for Students:** Link student attendance, certificate uploads, and On-Duty (OD) approvals step-by-step.
|
||||
* **Security & Accessibility:** Provide separate, secure authentication models for internal students, external students, coordinators (faculty/HOD), and administrators.
|
||||
|
||||
---
|
||||
|
||||
## 2. Technology Stack & Integrations
|
||||
|
||||
The platform is built using a modern, fast, and secure web development stack:
|
||||
|
||||
| Layer | Technology | Description |
|
||||
| :--- | :--- | :--- |
|
||||
| **Frontend Framework** | React (v19), TypeScript, Vite | Multi-page single-page application (SPA) state-managed structure. |
|
||||
| **Styling & Icons** | Tailwind CSS (CDN), FontAwesome | Curated responsive styling using professional color palettes (Deep Blue `#004a99` and Academic Orange `#f97316`). |
|
||||
| **Backend & Auth** | Supabase (PostgreSQL, Storage, Auth) | Manages authentication, row-level security (RLS), document uploads, and database queries. |
|
||||
| **PDF Generation** | `jspdf` & `jspdf-autotable` | Compiles detailed, printable event proposals containing budget sheets, schedules, and resource details. |
|
||||
| **Layout Tools** | `html2canvas` | Used to capture ticket layouts for student downloads. |
|
||||
| **Fonts** | Google Fonts (Inter, Playfair Display) | Custom typography reflecting academic elegance. |
|
||||
|
||||
---
|
||||
|
||||
## 3. Database Schema
|
||||
|
||||
The database relies on a PostgreSQL schema managed via Supabase. Below are the primary tables and relations:
|
||||
|
||||
### `Studentusers` (Internal Students)
|
||||
* `id` (UUID, Primary Key, references auth.users)
|
||||
* `name` (Text)
|
||||
* `email` (Text, Unique)
|
||||
* `reg_no` (Text, Unique)
|
||||
* `phone` (Text)
|
||||
* `department` (Text)
|
||||
* `year` (Text)
|
||||
* `section` (Text)
|
||||
* `college_name` (Text - Default: 'RAJALAKSHMI INSTITUTE OF TECHNOLOGY')
|
||||
* `updated_at` (Timestamp)
|
||||
|
||||
### `externalusers` (External Students)
|
||||
* `id` (UUID, Primary Key, references auth.users)
|
||||
* `name` (Text)
|
||||
* `email` (Text, Unique)
|
||||
* `reg_no` (Text)
|
||||
* `phone` (Text)
|
||||
* `department` (Text)
|
||||
* `year` (Text)
|
||||
* `section` (Text)
|
||||
* `college` (Text)
|
||||
* `college_location` (Text)
|
||||
* `gender` (Text)
|
||||
* `updated_at` (Timestamp)
|
||||
|
||||
### `Facultyusers` (Coordinators & HODs)
|
||||
* `id` (UUID, Primary Key, references auth.users)
|
||||
* `name` (Text)
|
||||
* `email` (Text, Unique)
|
||||
* `dept` (Text)
|
||||
* `role` (Text - e.g., 'Faculty', 'HOD', 'System Admin')
|
||||
* `phone` (Text)
|
||||
* `profile_photo` (Text - URL)
|
||||
* `updated_at` (Timestamp)
|
||||
|
||||
### `Adminusers` (Principal / Director / Administrators)
|
||||
* `id` (UUID, Primary Key, references auth.users)
|
||||
* `name` (Text)
|
||||
* `email` (Text, Unique)
|
||||
* `dept` (Text - 'Administration')
|
||||
* `updated_at` (Timestamp)
|
||||
|
||||
### `events`
|
||||
* `id` (UUID, Primary Key)
|
||||
* `title` (Text)
|
||||
* `location` (Text - Venue name)
|
||||
* `date` (Text - Standard format)
|
||||
* `category` (Text - 'TECHNICAL' \| 'NON-TECHNICAL' \| 'WORKSHOP' \| 'CENTRE-ACTIVITY')
|
||||
* `domain` (Text - references domains.name)
|
||||
* `pricing_type` (Text - 'FREE' \| 'PAID')
|
||||
* `coordinator` (Text - Coordinator Name)
|
||||
* `club` (Text)
|
||||
* `image` (Text - Supabase storage URL)
|
||||
* `status` (Text - 'Scheduled' \| 'Event Ongoing' \| 'Completed')
|
||||
* `max_participants` (Integer)
|
||||
* `registration_deadline` (Timestamp)
|
||||
* `duration_days` (Integer)
|
||||
* `event_summary` (Text)
|
||||
* `is_team_event` (Boolean)
|
||||
* `team_size_limit` (Integer)
|
||||
* `team_composition` (Text - 'MIXED' \| 'INTER_DEPT')
|
||||
* `participant_type` (Text - 'INTERNAL' \| 'EXTERNAL' \| 'BOTH')
|
||||
* `verification_status` (Text - 'PENDING' \| 'PENDING_HOD' \| 'PENDING_ADMIN' \| 'APPROVED' \| 'REJECTED')
|
||||
* `refreshment_expense` (Numeric)
|
||||
* `transportation_expense` (Numeric)
|
||||
* `session_coverage_fee` (Numeric)
|
||||
* `total_expense` (Numeric)
|
||||
* `conducting_dept` (Text)
|
||||
* `created_by` (UUID, references auth.users)
|
||||
* `request_by_faculty` (Timestamp)
|
||||
* `request_by_hod` (Timestamp)
|
||||
|
||||
### `event_schedule` (Session/Batch Slots)
|
||||
* `id` (UUID, Primary Key)
|
||||
* `event_id` (UUID, references events.id)
|
||||
* `day_idx` (Integer)
|
||||
* `batch_idx` (Integer)
|
||||
* `date` (Text)
|
||||
* `start_time` (Text)
|
||||
* `end_time` (Text)
|
||||
|
||||
### `resource_persons` (Guest Details)
|
||||
* `id` (UUID, Primary Key)
|
||||
* `event_id` (UUID, references events.id)
|
||||
* `day_idx` (Integer)
|
||||
* `batch_idx` (Integer)
|
||||
* `schedule_id` (UUID, references event_schedule.id)
|
||||
* `type` (Text - 'INTERNAL' \| 'EXTERNAL')
|
||||
* `name` (Text)
|
||||
* `dept` (Text)
|
||||
* `college_name` (Text)
|
||||
* `phone` (Text)
|
||||
* `email` (Text)
|
||||
|
||||
### `event_dept_limits` (Departmental Seat Allocations)
|
||||
* `id` (UUID, Primary Key)
|
||||
* `event_id` (UUID, references events.id)
|
||||
* `department` (Text)
|
||||
* `max_seats` (Integer)
|
||||
* `section_limits` (JSONB - Maps sections (A, B, C...) to integer limits)
|
||||
|
||||
### `registrations` (Student Bookings)
|
||||
* `id` (Text, Primary Key - Structured as `${userId}_${eventId}`)
|
||||
* `user_id` (UUID, references auth.users)
|
||||
* `event_id` (UUID, references events.id)
|
||||
* `user_email` (Text)
|
||||
* `user_name` (Text)
|
||||
* `reg_no` (Text)
|
||||
* `phone` (Text)
|
||||
* `gender` (Text)
|
||||
* `dept` (Text)
|
||||
* `section` (Text)
|
||||
* `year` (Text)
|
||||
* `college` (Text)
|
||||
* `payment_status` (Text - 'PENDING' \| 'COMPLETED')
|
||||
* `registered_at` (Timestamp)
|
||||
* `team_code` (Text)
|
||||
* `team_name` (Text)
|
||||
* `is_team_leader` (Boolean)
|
||||
* `certification_url` (Text)
|
||||
* `certification_status` (Text - 'PENDING' \| 'APPROVED' \| 'REJECTED')
|
||||
* `od_url` (Text)
|
||||
* `od` (Boolean - OD approval)
|
||||
|
||||
### `announcements` (Notice Board)
|
||||
* `id` (UUID, Primary Key)
|
||||
* `title` (Text)
|
||||
* `message` (Text)
|
||||
* `type` (Text - 'DELAY' \| 'INFO' \| 'URGENT' \| 'ENDED' \| 'ONGOING')
|
||||
* `event_id` (UUID, references events.id)
|
||||
* `timestamp` (Timestamp)
|
||||
* `expires_at` (Timestamp)
|
||||
|
||||
### `special_events` (Symposiums & External Links)
|
||||
* `id` (UUID, Primary Key)
|
||||
* `title` (Text)
|
||||
* `description` (Text)
|
||||
* `link` (Text)
|
||||
* `created_by` (UUID)
|
||||
* `is_active` (Boolean)
|
||||
* `verification_status` (Text - 'PENDING' \| 'APPROVED' \| 'REJECTED')
|
||||
|
||||
### `domains`
|
||||
* `id` (UUID, Primary Key)
|
||||
* `name` (Text, Unique)
|
||||
* `category` (Text)
|
||||
* `image` (Text)
|
||||
* `status` (Text - 'PENDING' \| 'APPROVED' \| 'REJECTED')
|
||||
* `description` (Text)
|
||||
|
||||
---
|
||||
|
||||
## 4. Operational Workflows & Key Logic
|
||||
|
||||
### A. Core Blockage (Preventing Registration Overhead)
|
||||
To maintain academic integrity, a student **cannot register for any new events** if they have an unfinalized event from the past.
|
||||
* **Condition:** If a student is registered for an event that has completed (`status === 'Completed'`), they must upload their certificate and obtain coordinator approval (`certification_status === 'APPROVED'`) and OD status before the system allows them to click "Register" on any future catalog events.
|
||||
|
||||
### B. Overlap Prevention (Intelligent Venue Booking)
|
||||
During event creation, coordinates are validated against active bookings:
|
||||
* **Conflict Condition:** When a coordinator selects a venue and date, the system queries existing approved/pending events. If another event occupies the same location, and the date ranges overlap based on the `durationDays`, that venue will be flagged as "Booked" or "Under Verification" in the dropdown, disabling selection.
|
||||
|
||||
### C. Quota-Constrained Bookings (Sectional & Departmental Caps)
|
||||
The event organizer can set constraints for RIT students:
|
||||
1. **Department Limits:** Limits registration to `X` seats per department.
|
||||
2. **Section Limits:** Further restricts seats down to individual sections (e.g., CSE section `A` gets 10 seats, section `B` gets 5 seats).
|
||||
3. **Validation:** When a student attempts registration, the system evaluates their profile (department, section) and checks the respective quota counts in the `registrations` table. If the quota is full, booking is blocked.
|
||||
|
||||
### D. Team play Alliance (Team Registrations)
|
||||
* For team events, the first student registers and clicks "Form Team", which inserts a random 6-character alphanumeric code (`team_code`) and designates them as `is_team_leader`.
|
||||
* Subsequent students register for the event, click "Join Team", and input the code. The system verifies the team is not full (respecting `teamSizeLimit`) and matches `INTER_DEPT` composition requirements before assigning the `team_code` to their registration.
|
||||
|
||||
### E. Proposal PDF Compile
|
||||
Coordinators and Admin can generate a standardized PDF proposal containing:
|
||||
1. General overview (departments, clubs, metadata).
|
||||
2. Logistical details (location, dates, capacity, formats).
|
||||
3. Day-by-day itineraries mapped to guest resource persons and timings.
|
||||
4. Departmental seat allocation tables.
|
||||
5. Financial projections (Refreshment + Transportation + Session Fees = Total projected budget).
|
||||
6. Signature/Verification box showing HOD & Admin approval timestamps.
|
||||
|
||||
---
|
||||
|
||||
## 5. Portal Flow & Visual Site Map
|
||||
|
||||
### A. Navigational Site Map Structure
|
||||
|
||||
```
|
||||
[WELCOME GATEWAY]
|
||||
│
|
||||
├── STUDENT PORTAL ────► [STUDENT AUTH (Login / Sign Up)]
|
||||
│ │
|
||||
│ ├── HOME DASHBOARD (Notices, Banner, Stats)
|
||||
│ ├── EVENTS CATALOG (Details, Booking, Team Play)
|
||||
│ ├── REGISTRATIONS (QR Tickets, Track Progress)
|
||||
│ ├── STATUS TRACKER (Detailed Lifecycle Step Visualizer)
|
||||
│ └── PROFILE VIEW (Update Info, Uploaded Credentials)
|
||||
│
|
||||
├── FACULTY PORTAL ────► [COORDINATOR AUTH (Faculty / HOD)]
|
||||
│ │
|
||||
│ ├── OVERVIEW (Metrics & Quick Gateways)
|
||||
│ ├── GENERATE EVENTS (Normal, Special, Domains)
|
||||
│ ├── CREATIONS REGISTRY (Manage Owned Events, Edit/Delete)
|
||||
│ ├── STATUS TRACKER (Verify Certificates, Audit ODs)
|
||||
│ ├── EVENT STATUS CONTROL (Announcements, Status Updates)
|
||||
│ ├── ATTENDANCE CONSOLE (Mark Student Attendance)
|
||||
│ ├── PARTICIPANTS console (Download Rosters)
|
||||
│ ├── PROFILE / LOG OUT
|
||||
│ └── HOD VERIFICATION (HOD only - Review Departmental Proposals)
|
||||
│
|
||||
└── ADMIN PORTAL ──────► [ADMIN AUTH (Principal / Director / System Admin)]
|
||||
│
|
||||
├── OVERVIEW (System Metrics)
|
||||
├── VERIFICATION QUEUE (Standard Events, Special Events, Domains)
|
||||
├── USER MANAGEMENT CONSOLE (Update Profiles, Change Roles)
|
||||
├── VENUE TRACKER CALENDAR (Visual Schedule & Collision Map)
|
||||
├── STATUS TRACKER (Audit all college certifications)
|
||||
├── GLOBAL NOTICES (Institutional notices)
|
||||
└── PROFILE / LOG OUT
|
||||
```
|
||||
|
||||
### B. High-Fidelity Mermaid Routing Diagram
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
%% Base Gateways
|
||||
Welcome[Welcome Screen] -->|Select STUDENT| StudentAuth[Student Authentication]
|
||||
Welcome -->|Select COORDINATOR| FacultyAuth[Faculty / HOD Authentication]
|
||||
Welcome -->|Select ADMIN| AdminAuth[System Admin Authentication]
|
||||
|
||||
%% Student Pathways
|
||||
StudentAuth -->|Authenticated| StudentDash[Student Dashboard]
|
||||
StudentDash --> SHome[Home Dashboard]
|
||||
StudentDash --> SEvents[Events Catalog]
|
||||
StudentDash --> SRegs[Registrations & QR Tickets]
|
||||
StudentDash --> STrack[Event Status Tracker]
|
||||
StudentDash --> SProfile[Student Profile]
|
||||
|
||||
SEvents -->|Register & Form/Join Team| STrack
|
||||
SRegs -->|Verify Status / Upload Certificate| STrack
|
||||
|
||||
%% Faculty Pathways
|
||||
FacultyAuth -->|Select Access Level| FacultyLvl{Access Level}
|
||||
FacultyLvl -->|Faculty| FacultyOverview[Faculty Dashboard]
|
||||
FacultyLvl -->|HOD| HODOverview[Faculty Dashboard + HOD Verification]
|
||||
|
||||
FacultyOverview --> FHome[Overview & Statistics]
|
||||
FacultyOverview --> FCreate[Create Event Form]
|
||||
FacultyOverview --> FCreations[Manage Creations]
|
||||
FacultyOverview --> FTracker[Status Tracker - Certificate Audit]
|
||||
FacultyOverview --> FStatus[Event Status & Notices]
|
||||
FacultyOverview --> FAttendance[Attendance Console]
|
||||
FacultyOverview --> FParticipants[Participants Console]
|
||||
FacultyOverview --> FProfile[Faculty Profile]
|
||||
|
||||
HODOverview --> HVerify[HOD Verification Panel]
|
||||
HVerify -->|Accept Department Request| FTracker
|
||||
HVerify -->|Reject Request| FCreations
|
||||
|
||||
%% Admin Pathways
|
||||
AdminAuth --> AdminOverview[Admin Hub]
|
||||
AdminOverview --> AHome[System Overview]
|
||||
AdminOverview --> AVerifyQueue[Verification Queue - Events/Domains/Special]
|
||||
AdminOverview --> AUsers[User Management Console]
|
||||
AdminOverview --> AVenue[Venue Tracker Calendar]
|
||||
AdminOverview --> ATracker[Status Tracker]
|
||||
AdminOverview --> ANotices[Global Announcements]
|
||||
AdminOverview --> AProfile[Admin Profile]
|
||||
|
||||
%% Cross-Links
|
||||
FCreations -->|Submit Proposal| HVerify
|
||||
HVerify -->|Send to Admin Queue| AVerifyQueue
|
||||
AVerifyQueue -->|Approve Event| SEvents
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Directory Layout & Components Guide
|
||||
|
||||
### File & Component Inventory
|
||||
|
||||
* `App.tsx`: Main routing and authentication portal manager. Resolves session state, verifies metadata credentials, performs portal security checks, and loads global states.
|
||||
* `supabase.ts`: Supabase client configuration, administrative client helper, and helper functions for uploading media (images, certificates) directly to Supabase storage.
|
||||
* `types.ts`: TypeScript interfaces defining `Event`, `AppState`, `UserRole`, `StudentRequest`, `Ticket`, `ResourcePerson`, `EventSchedule`, `Announcement`, and `UserProfile`.
|
||||
* `constants.tsx`: Lists static categories, default college domain mappings, clubs, and default showcase events.
|
||||
* `utils/pdfGenerator.ts`: Handles compiling and exporting the official document proposals for verified events using `jspdf`.
|
||||
|
||||
#### Components Directory (`/components/`):
|
||||
1. `WelcomeScreen.tsx`: Entrance portal page displaying entry gates for students, faculty coordinators, and admins.
|
||||
2. `LoginForm.tsx`: sliding dual-panel login and sign up form handling internal/external student data collection and role checks.
|
||||
3. `Dashboard.tsx`: Base container routing student pages.
|
||||
4. `HomeDashboard.tsx`: Displays active announcements, stats, special events, and links to catalogs.
|
||||
5. `Hero.tsx`: Dynamic slide search and welcome banner for students.
|
||||
6. `UpcomingEventsSlider.tsx`: Auto-scrolling showcase slider displaying upcoming events.
|
||||
7. `AboutHubSection.tsx`: Summary presentation card describing the Events Hub platform.
|
||||
8. `StatsSection.tsx`: Shows real-time totals of events, participants, and clubs.
|
||||
9. `AboutSection.tsx`: Institutional overview details, college details, and guidelines.
|
||||
10. `ContactSection.tsx`: Map coordinates, support addresses, and telephone details for college portals.
|
||||
11. `EventList.tsx`: Searchable list displaying approved events by domain filter and category.
|
||||
12. `EventCard.tsx`: Display card for events containing metadata triggers, register commands, and status links.
|
||||
13. `RegistrationsView.tsx`: Displays registered ticket vouchers. Generates ticket QR code cards capturing registration details, allowing downloading ticket PDFs.
|
||||
14. `StatusTrackerView.tsx`: visual step progress checker showing the student their certificate review, attendance logs, and OD approvals.
|
||||
15. `ProfileView.tsx`: Form for students to edit profile settings and view academic transcripts.
|
||||
16. `AdminLandingPage.tsx`: Base dashboard routing all Coordinator actions.
|
||||
17. `CreateEventForm.tsx`: Multi-step event proposal compiler managing budgets, itineraries, and quotas.
|
||||
18. `CreateSpecialEventsView.tsx`: Allows coordinators to publish external program links.
|
||||
19. `CreateDomainView.tsx`: Form to submit new technical/non-technical domains for college audit.
|
||||
20. `AdminStatusTrackerView.tsx`: Review board for coordinators/admins to check student certificate uploads, verify details, and approve OD statuses.
|
||||
21. `AdminEventStatusView.tsx`: Controls notifications, notice updates, and events status transitions.
|
||||
22. `FacultyParticipantsView.tsx`: Table listing students registered for an event with search/filter.
|
||||
23. `FacultyAttendanceView.tsx`: Logs session-by-session student attendance check-ins.
|
||||
24. `FacultyProfileView.tsx`: Base profile for faculty.
|
||||
25. `FacultyDashboard.tsx`: Console routing Admin actions.
|
||||
26. `UserManagementView.tsx`: Table displaying all registered users. Admins can edit profile metadata, manually add students, or alter roles.
|
||||
27. `TicketVerificationView.tsx`: Camera/code scanner allowing organizers to scan student ticket QR codes and update status in real-time.
|
||||
28. `Footer.tsx`: Institutional styled footer.
|
||||
29. `PortalAnimation.tsx`: Page transitions and loading animation overlays.
|
||||
20
RIT-EVENT-MANAGEMENT--main/README.md
Normal file
20
RIT-EVENT-MANAGEMENT--main/README.md
Normal file
@@ -0,0 +1,20 @@
|
||||
<div align="center">
|
||||
<img width="1200" height="475" alt="GHBanner" src="https://github.com/user-attachments/assets/0aa67016-6eaf-458a-adb2-6e31a0763ed6" />
|
||||
</div>
|
||||
|
||||
# Run and deploy your AI Studio app
|
||||
|
||||
This contains everything you need to run your app locally.
|
||||
|
||||
View your app in AI Studio: https://ai.studio/apps/18ff5fc9-efef-4851-8197-ca99100fcd5f
|
||||
|
||||
## Run Locally
|
||||
|
||||
**Prerequisites:** Node.js
|
||||
|
||||
|
||||
1. Install dependencies:
|
||||
`npm install`
|
||||
2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key
|
||||
3. Run the app:
|
||||
`npm run dev`
|
||||
72
RIT-EVENT-MANAGEMENT--main/components/AboutHubSection.tsx
Normal file
72
RIT-EVENT-MANAGEMENT--main/components/AboutHubSection.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import React from 'react';
|
||||
import { motion } from 'motion/react';
|
||||
|
||||
const AboutHubSection: React.FC = () => {
|
||||
return (
|
||||
<section className="w-full bg-[#F9FAFB] py-20 px-6 md:px-12 lg:px-20 overflow-hidden">
|
||||
<div className="max-w-7xl mx-auto grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-20 items-center">
|
||||
|
||||
{/* Left Side: Image & Floating Card */}
|
||||
<div className="relative w-full h-[500px] lg:h-[600px] group perspective-1000">
|
||||
<motion.img
|
||||
initial={{ opacity: 0, x: -50 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
transition={{ duration: 0.8, ease: "easeOut" }}
|
||||
viewport={{ once: true }}
|
||||
src="https://cache.careers360.mobi/media/presets/720X480/colleges/social-media/media-gallery/3425/2021/6/16/DSC08602.JPG"
|
||||
alt="RIT Campus Collaboration"
|
||||
className="w-full h-full object-cover rounded-3xl shadow-2xl transition-transform duration-700 group-hover:scale-[1.02]"
|
||||
/>
|
||||
|
||||
{/* Floating Card */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 50, scale: 0.9 }}
|
||||
whileInView={{ opacity: 1, y: 0, scale: 1 }}
|
||||
whileHover={{ y: -5 }}
|
||||
transition={{ duration: 0.6, delay: 0.3, type: "spring", stiffness: 100 }}
|
||||
viewport={{ once: true }}
|
||||
className="absolute -bottom-6 -right-6 md:bottom-10 md:-right-10 bg-[#2D3748] text-[#f97316] p-6 md:p-8 rounded-xl shadow-xl max-w-[280px] z-10 border-l-4 border-[#f97316]"
|
||||
>
|
||||
<p className="text-xl md:text-2xl font-serif font-bold leading-tight tracking-wide">
|
||||
INNOVATION
|
||||
</p>
|
||||
<p className="text-sm md:text-base text-gray-300 mt-2 font-medium">
|
||||
Where creativity meets execution. Fueling the future of tech.
|
||||
</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Right Side: Content */}
|
||||
<div className="flex flex-col justify-center space-y-8">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.2 }}
|
||||
viewport={{ once: true }}
|
||||
className="space-y-4"
|
||||
>
|
||||
<h4 className="text-[#f97316] font-serif tracking-widest text-sm font-bold uppercase">
|
||||
Our Legacy
|
||||
</h4>
|
||||
<h2 className="text-4xl md:text-5xl lg:text-6xl font-serif text-[#2D3748] leading-tight">
|
||||
What is <span className="italic text-[#f97316]">RIT EVENTS HUB?</span>
|
||||
</h2>
|
||||
</motion.div>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.4 }}
|
||||
viewport={{ once: true }}
|
||||
className="text-[#2D3748] text-lg leading-relaxed max-w-xl"
|
||||
>
|
||||
The RIT Events Hub is your centralized gateway to campus life, designed to simplify how you discover and participate in events. Whether it's a technical hackathon, a cultural fest, or a workshop, our platform brings everything to your fingertips. With seamless registration, real-time updates, and personalized recommendations, we ensure you never miss an opportunity to learn, compete, and grow. Join a community where accessing innovation is as easy as a single click.
|
||||
</motion.p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default AboutHubSection;
|
||||
133
RIT-EVENT-MANAGEMENT--main/components/AboutSection.tsx
Normal file
133
RIT-EVENT-MANAGEMENT--main/components/AboutSection.tsx
Normal file
@@ -0,0 +1,133 @@
|
||||
import React from 'react';
|
||||
|
||||
const AboutSection: React.FC = () => {
|
||||
return (
|
||||
<section className="py-24 px-6 md:px-12 lg:px-24 bg-white">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="mb-8">
|
||||
<h3 className="text-[#f97316] font-bold tracking-widest uppercase text-xl mb-2 relative inline-block">
|
||||
ABOUT
|
||||
<span className="absolute -bottom-2 left-0 w-full h-1 bg-[#f97316]"></span>
|
||||
</h3>
|
||||
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold text-[#1e3a8a] mt-6 mb-8">
|
||||
Rajalakshmi Institute of Technology (RIT)
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col lg:flex-row gap-12 items-start">
|
||||
<div className="lg:w-1/2 text-gray-700 leading-relaxed text-lg text-justify">
|
||||
<p className="mb-4">
|
||||
Rajalakshmi Institute of Technology ( An Autonomous Institution) is one of the best engineering colleges in Chennai and is part of Rajalakshmi Institutions, which has been synonymous with providing excellence in higher education to students for many years.
|
||||
</p>
|
||||
<p className="mb-4">
|
||||
Rajalakshmi Institute of Technology was established in 2008. RIT is accredited with highest grade of A++ by NAAC. RIT is affiliated with Anna University Chennai. It is one of the <a href="#" className="text-blue-500 hover:underline">AICTE-approved colleges in Chennai</a> New Delhi, and also offers NBA-approved courses.
|
||||
</p>
|
||||
</div>
|
||||
<div className="lg:w-1/2 w-full">
|
||||
<img
|
||||
src="https://cache.careers360.mobi/media/presets/720X480/colleges/social-media/media-gallery/3425/2021/6/16/DSC05418.JPG"
|
||||
alt="Rajalakshmi Institute of Technology"
|
||||
className="w-full h-auto object-cover shadow-lg"
|
||||
referrerPolicy="no-referrer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Graduate Programmes Section */}
|
||||
<div className="mt-24">
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-[#1e3a8a] text-center mb-16">
|
||||
Graduate Programmes offered
|
||||
</h2>
|
||||
|
||||
<div className="flex flex-col lg:flex-row gap-12 items-start">
|
||||
<div className="lg:w-1/2 w-full">
|
||||
<img
|
||||
src="https://ritchennai.org/img/image/slider-m-3.jpg"
|
||||
alt="RIT Students"
|
||||
className="w-full h-auto object-cover shadow-lg rounded-lg"
|
||||
referrerPolicy="no-referrer"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="lg:w-1/2 w-full space-y-8">
|
||||
<div>
|
||||
<h3 className="text-xl font-bold text-[#1e3a8a] mb-4">UG Programmes</h3>
|
||||
<ul className="space-y-2 text-gray-700 list-disc pl-5 marker:text-[#f97316]">
|
||||
<li>B.E. Computer Science & Engineering</li>
|
||||
<li>B.E. Computer Science & Engineering(AI&ML)</li>
|
||||
<li>B.E. Computer & Communication Engineering</li>
|
||||
<li>B.E. Electronics & Communication Engineering</li>
|
||||
<li>B.E. Mechanical Engineering</li>
|
||||
<li>B.E. Electronic Engineering (VLSI)</li>
|
||||
<li>B.Tech. Artificial Intelligence & Data Science</li>
|
||||
<li>B.Tech. Computer Science and Business Systems</li>
|
||||
<li>B.Tech Bio Technology</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-xl font-bold text-[#1e3a8a] mb-4">PG Programmes</h3>
|
||||
<ul className="space-y-2 text-gray-700 list-disc pl-5 marker:text-[#f97316]">
|
||||
<li>M.E. Electronics and Communication Engineering (VLSI Design)</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-xl font-bold text-[#1e3a8a] mb-4">Anna University Approved Research Institute</h3>
|
||||
<p className="text-gray-700">
|
||||
Ph.D. Programmes are offered across all Engineering, Technology, Science & Humanities disciplines
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Campus Life & Events Section */}
|
||||
<div className="mt-24">
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-[#1e3a8a] text-center mb-16">
|
||||
Campus Life & Events
|
||||
</h2>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
{/* Left Column: YouTube Videos */}
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="w-full rounded-xl overflow-hidden shadow-lg aspect-video">
|
||||
<iframe
|
||||
className="w-full h-full"
|
||||
src="https://www.youtube.com/embed/5_L5JMb-a5k"
|
||||
title="RAJALAKSHMI INSTITUTE OF TECHNOLOGY"
|
||||
frameBorder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
allowFullScreen
|
||||
></iframe>
|
||||
</div>
|
||||
<div className="w-full rounded-xl overflow-hidden shadow-lg aspect-video">
|
||||
<iframe
|
||||
className="w-full h-full"
|
||||
src="https://www.youtube.com/embed/RfMkWSZHw5o"
|
||||
title="Inauguration of The Grover Centre for Quantum Computing"
|
||||
frameBorder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
allowFullScreen
|
||||
></iframe>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Column: Instagram Post */}
|
||||
<div className="w-full h-full min-h-[600px] rounded-xl overflow-hidden shadow-lg bg-white flex items-center justify-center">
|
||||
<iframe
|
||||
className="w-full h-full"
|
||||
src="https://www.instagram.com/p/DCs0891p6HY/embed"
|
||||
frameBorder="0"
|
||||
scrolling="no"
|
||||
allowtransparency="true"
|
||||
></iframe>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default AboutSection;
|
||||
@@ -0,0 +1,33 @@
|
||||
import React from 'react';
|
||||
|
||||
const AccreditationsSection: React.FC = () => {
|
||||
// No longer needed: list of individual logos replaced by a single unified picture.
|
||||
// const accreditations = [...];
|
||||
|
||||
return (
|
||||
<section className="py-20 bg-white/50 backdrop-blur-sm rounded-[2rem] shadow-xl border border-white/20 my-12 overflow-hidden relative">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-blue-50/50 via-transparent to-orange-50/50 pointer-events-none"></div>
|
||||
|
||||
<div className="max-w-7xl mx-auto px-8 relative z-10">
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-sm font-black text-[#f97316] uppercase tracking-[0.3em] mb-3">Our Strategic Partners</h2>
|
||||
<h3 className="text-3xl md:text-4xl font-serif text-[#1e3a8a] font-bold">Global Accreditations & Collaborations</h3>
|
||||
<div className="w-20 h-1 bg-gradient-to-r from-[#f97316] to-orange-400 mx-auto mt-6 rounded-full"></div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center items-center">
|
||||
<div className="relative w-full max-w-5xl mx-auto px-4">
|
||||
<img
|
||||
src="https://raw.githubusercontent.com/Sachin-627/RIT-COLLABORATORS-PIC/main/Screenshot%202026-04-01%20120832.png"
|
||||
alt="Global Accreditations & Collaborations"
|
||||
className="w-full h-auto object-contain rounded-2xl shadow-lg border border-white/20 transition-transform duration-500 hover:scale-[1.01]"
|
||||
referrerPolicy="no-referrer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccreditationsSection;
|
||||
429
RIT-EVENT-MANAGEMENT--main/components/AdminEventStatusView.tsx
Normal file
429
RIT-EVENT-MANAGEMENT--main/components/AdminEventStatusView.tsx
Normal file
@@ -0,0 +1,429 @@
|
||||
import React, { useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { Event, Announcement } from '../types';
|
||||
|
||||
interface AdminEventStatusViewProps {
|
||||
events: Event[];
|
||||
announcements: Announcement[];
|
||||
onAddAnnouncement: (ann: Announcement) => Promise<void>;
|
||||
onUpdateAnnouncement?: (ann: Announcement) => Promise<void>;
|
||||
onDeleteAnnouncement: (id: string) => Promise<void>;
|
||||
onUpdateEvent: (event: Event) => void;
|
||||
onBack: () => void;
|
||||
currentUserId?: string;
|
||||
localRegistrations?: any[];
|
||||
}
|
||||
|
||||
type ExpiryOption = '1H' | '6H' | '24H' | 'NEVER' | 'CUSTOM';
|
||||
|
||||
const AdminEventStatusView: React.FC<AdminEventStatusViewProps> = ({
|
||||
events,
|
||||
announcements,
|
||||
onAddAnnouncement,
|
||||
onUpdateAnnouncement,
|
||||
onDeleteAnnouncement,
|
||||
onUpdateEvent,
|
||||
onBack,
|
||||
currentUserId,
|
||||
localRegistrations = []
|
||||
}) => {
|
||||
const [selectedEventId, setSelectedEventId] = useState('');
|
||||
const [message, setMessage] = useState('');
|
||||
const [type, setType] = useState<Announcement['type']>('INFO');
|
||||
const [expiryOption, setExpiryOption] = useState<ExpiryOption>('NEVER');
|
||||
const [customExpiry, setCustomExpiry] = useState('');
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [deletingId, setDeletingId] = useState<string | null>(null);
|
||||
const [editingId, setEditingId] = useState<string | null>(null);
|
||||
const [showOverwriteConfirm, setShowOverwriteConfirm] = useState<Announcement | null>(null);
|
||||
const [confirmDeleteId, setConfirmDeleteId] = useState<string | null>(null);
|
||||
|
||||
const calculateExpiry = (): string | null => {
|
||||
const now = new Date();
|
||||
switch (expiryOption) {
|
||||
case '1H': return new Date(now.getTime() + 60 * 60 * 1000).toISOString();
|
||||
case '6H': return new Date(now.getTime() + 6 * 60 * 60 * 1000).toISOString();
|
||||
case '24H': return new Date(now.getTime() + 24 * 60 * 60 * 1000).toISOString();
|
||||
case 'CUSTOM': return customExpiry ? new Date(customExpiry).toISOString() : null;
|
||||
default: return null;
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = (ann: Announcement) => {
|
||||
setEditingId(ann.id);
|
||||
setSelectedEventId(ann.eventId || '');
|
||||
const cleanMessage = ann.message.includes(']') ? ann.message.split(']').slice(1).join(']').trim() : ann.message;
|
||||
setMessage(cleanMessage);
|
||||
setType(ann.type);
|
||||
setExpiryOption(ann.expiresAt ? 'CUSTOM' : 'NEVER');
|
||||
if (ann.expiresAt) setCustomExpiry(new Date(ann.expiresAt).toISOString().slice(0, 16));
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
};
|
||||
|
||||
const cancelEdit = () => {
|
||||
setEditingId(null);
|
||||
setMessage('');
|
||||
setSelectedEventId('');
|
||||
setExpiryOption('NEVER');
|
||||
setCustomExpiry('');
|
||||
};
|
||||
|
||||
const processDelete = async () => {
|
||||
if (!confirmDeleteId) return;
|
||||
const id = confirmDeleteId;
|
||||
setConfirmDeleteId(null);
|
||||
setDeletingId(id);
|
||||
try {
|
||||
await onDeleteAnnouncement(id);
|
||||
} catch (err: any) {
|
||||
console.error("UI Delete Trigger Error:", err);
|
||||
alert("Database failed to delete. Check console for error.");
|
||||
} finally {
|
||||
setDeletingId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const executeSubmit = async (overrideId?: string) => {
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
const selectedEvent = events.find(ev => ev.id === selectedEventId);
|
||||
const expiresAt = calculateExpiry();
|
||||
const targetId = overrideId || editingId;
|
||||
|
||||
const announcementPayload: Announcement = {
|
||||
id: targetId || '',
|
||||
title: type === 'DELAY' ? 'DELAY ALERT' : type === 'URGENT' ? 'URGENT' : type === 'ENDED' ? 'CONCLUDED' : type === 'ONGOING' ? 'LIVE NOW' : 'NOTICE',
|
||||
message: selectedEvent ? `[${selectedEvent.title}] ${message}` : message,
|
||||
type,
|
||||
timestamp: new Date().toISOString(),
|
||||
expiresAt: expiresAt,
|
||||
eventId: selectedEventId || undefined
|
||||
};
|
||||
|
||||
if (targetId && onUpdateAnnouncement) {
|
||||
await onUpdateAnnouncement(announcementPayload);
|
||||
} else {
|
||||
await onAddAnnouncement(announcementPayload);
|
||||
}
|
||||
|
||||
if (type === 'ENDED' && selectedEvent) {
|
||||
onUpdateEvent({ ...selectedEvent, status: 'Completed' });
|
||||
} else if (type === 'ONGOING' && selectedEvent) {
|
||||
onUpdateEvent({ ...selectedEvent, status: 'Event Ongoing' });
|
||||
}
|
||||
|
||||
cancelEdit();
|
||||
setShowOverwriteConfirm(null);
|
||||
} catch (err) {
|
||||
console.error("Broadcast operation failed:", err);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownloadExcel = () => {
|
||||
if (!selectedEventId || selectedEventId === 'GENERAL') return;
|
||||
const event = events.find(e => e.id === selectedEventId);
|
||||
if (!event) return;
|
||||
|
||||
const filteredRegs = localRegistrations.filter(reg => String(reg.event_id) === String(selectedEventId));
|
||||
if (filteredRegs.length === 0) {
|
||||
alert("No participants found for this event.");
|
||||
return;
|
||||
}
|
||||
|
||||
const headers = ['Name', 'Registration No', 'Department', 'Year', 'Email', 'Phone', 'College'];
|
||||
const rows = filteredRegs.map(r => [
|
||||
r.user_name || 'N/A',
|
||||
`\t${r.reg_no || 'N/A'}`,
|
||||
r.dept || 'N/A',
|
||||
r.year || 'N/A',
|
||||
r.email || 'N/A',
|
||||
r.phone || 'N/A',
|
||||
r.college || 'RAJALAKSHMI INSTITUTE OF TECHNOLOGY'
|
||||
]);
|
||||
|
||||
const csvContent = [
|
||||
[`EVENT: ${event.title}`].join(','),
|
||||
[`EXPORTED AT: ${new Date().toLocaleString()}`].join(','),
|
||||
[''],
|
||||
headers.join(','),
|
||||
...rows.map(row => row.map(cell => `"${cell}"`).join(','))
|
||||
].join('\n');
|
||||
|
||||
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.setAttribute('download', `${event.title.replace(/[^a-z0-9]/gi, '_').toLowerCase()}_participants.csv`);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
};
|
||||
|
||||
const handlePublish = async () => {
|
||||
if (!message.trim() || !selectedEventId) return;
|
||||
|
||||
if (selectedEventId !== 'GENERAL') {
|
||||
const event = events.find(e => e.id === selectedEventId);
|
||||
if (!event || event.created_by !== currentUserId) {
|
||||
alert("Unauthorized: Only the creator can broadcast for this event.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
executeSubmit();
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!message) return;
|
||||
|
||||
if ((type === 'ENDED' || type === 'ONGOING') && selectedEventId && !editingId) {
|
||||
const existingStatusNotice = announcements.find(a => a.eventId === selectedEventId && a.type === type);
|
||||
if (existingStatusNotice) {
|
||||
setShowOverwriteConfirm(existingStatusNotice);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
handlePublish();
|
||||
};
|
||||
|
||||
const getStatusColor = (t: Announcement['type']) => {
|
||||
switch (t) {
|
||||
case 'URGENT': return 'border-rose-500/30 text-rose-500 bg-rose-500/5';
|
||||
case 'DELAY': return 'border-amber-500/30 text-amber-500 bg-amber-500/5';
|
||||
case 'ENDED': return 'border-emerald-500/30 text-emerald-500 bg-emerald-500/5';
|
||||
case 'ONGOING': return 'border-purple-500/30 text-purple-500 bg-purple-500/5';
|
||||
default: return 'border-blue-500/30 text-blue-500 bg-blue-500/5';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto py-10 px-6 animate-in fade-in slide-in-from-bottom-10 duration-500">
|
||||
{/* Overwrite Confirmation Modal */}
|
||||
{showOverwriteConfirm && createPortal(
|
||||
<div className="fixed inset-0 z-[10001] flex items-center justify-center p-6 bg-slate-900/40 backdrop-blur-sm animate-in fade-in duration-300">
|
||||
<div className="bg-white border border-slate-200 rounded-[3.5rem] w-full max-w-md p-14 shadow-2xl flex flex-col items-center text-center animate-in zoom-in-95 duration-500">
|
||||
<div className="w-24 h-24 bg-amber-50 text-amber-500 rounded-full flex items-center justify-center text-4xl mb-8 border border-amber-100">
|
||||
<i className="fas fa-exclamation-triangle"></i>
|
||||
</div>
|
||||
<h3 className="text-3xl font-black text-slate-900 mb-3 uppercase tracking-tighter">Replace Previous Log?</h3>
|
||||
<p className="text-slate-500 font-bold text-[10px] uppercase tracking-[0.2em] mb-12 leading-relaxed">
|
||||
A conclusion log already exists for this scope.<br />
|
||||
Replacing it will overwrite the history entry.
|
||||
</p>
|
||||
<div className="flex gap-4 w-full">
|
||||
<button onClick={() => setShowOverwriteConfirm(null)} className="flex-1 py-5 bg-slate-50 text-slate-500 rounded-2xl font-black uppercase text-[10px] tracking-widest hover:bg-slate-100 hover:text-slate-900 transition-all border border-slate-200">Dismiss</button>
|
||||
<button onClick={() => executeSubmit(showOverwriteConfirm.id)} className="flex-1 py-5 bg-amber-500 text-white rounded-2xl font-black uppercase text-[10px] tracking-widest hover:bg-amber-600 transition-all shadow-xl shadow-amber-500/20">Overwrite</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
)}
|
||||
|
||||
{/* Delete Confirmation Modal */}
|
||||
{confirmDeleteId && createPortal(
|
||||
<div className="fixed inset-0 z-[10001] flex items-center justify-center p-6 bg-slate-900/40 backdrop-blur-sm animate-in fade-in duration-300">
|
||||
<div className="bg-white border border-slate-200 rounded-[3.5rem] w-full max-w-md p-14 shadow-2xl flex flex-col items-center text-center animate-in zoom-in-95 duration-500">
|
||||
<div className="w-24 h-24 bg-rose-50 text-rose-500 rounded-full flex items-center justify-center text-4xl mb-8 border border-rose-100">
|
||||
<i className="fas fa-trash-can"></i>
|
||||
</div>
|
||||
<h3 className="text-3xl font-black text-slate-900 mb-3 uppercase tracking-tighter">Remove Broadcast?</h3>
|
||||
<p className="text-slate-500 font-bold text-[10px] uppercase tracking-[0.2em] mb-12 leading-relaxed">
|
||||
This action is permanent and cannot be reversed.
|
||||
</p>
|
||||
<div className="flex gap-4 w-full">
|
||||
<button onClick={() => setConfirmDeleteId(null)} className="flex-1 py-5 bg-slate-50 text-slate-500 rounded-2xl font-black uppercase text-[10px] tracking-widest hover:bg-slate-100 hover:text-slate-900 transition-all border border-slate-200">Cancel</button>
|
||||
<button onClick={processDelete} className="flex-1 py-5 bg-rose-600 text-white rounded-2xl font-black uppercase text-[10px] tracking-widest hover:bg-rose-700 transition-all shadow-xl">Delete Now</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between mb-16 border-b border-slate-200 pb-8">
|
||||
<div>
|
||||
<h2 className="text-5xl font-black text-slate-900 tracking-tighter uppercase">ADMIN <span className="text-[#004a99]">PULSE</span></h2>
|
||||
<p className="text-[10px] font-black text-slate-500 uppercase tracking-[0.4em] mt-3 ml-1">Live Management of System Notifications</p>
|
||||
</div>
|
||||
<button onClick={onBack} className="group flex items-center gap-3 text-slate-400 hover:text-[#004a99] transition-all text-xs font-black uppercase tracking-widest bg-white border border-slate-200 px-6 py-3 rounded-full shadow-sm">
|
||||
<i className="fas fa-arrow-left transition-transform group-hover:-translate-x-1"></i> BACK
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-12 items-start">
|
||||
{/* Left Side: Creation Form */}
|
||||
<div className="lg:col-span-4 lg:sticky lg:top-24">
|
||||
<form onSubmit={handleSubmit} className="bg-white border border-slate-200 rounded-[3rem] p-10 space-y-8 shadow-sm">
|
||||
<div className="flex justify-between items-center">
|
||||
<h3 className="text-[10px] font-black text-slate-400 uppercase tracking-[0.3em]">{editingId ? 'Refining Broadcast' : 'New Broadcast'}</h3>
|
||||
{editingId && (
|
||||
<button type="button" onClick={cancelEdit} className="text-[9px] text-rose-500 font-black uppercase hover:underline">Cancel Edit</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-[9px] font-black text-slate-500 uppercase tracking-[0.2em] mb-3 ml-1">Target Scope</label>
|
||||
<div className="flex gap-2">
|
||||
<select
|
||||
className="flex-1 bg-white border border-slate-200 rounded-xl px-5 py-4 text-slate-900 focus:ring-2 focus:ring-[#004a99] outline-none font-bold text-xs appearance-none shadow-sm cursor-pointer"
|
||||
value={selectedEventId}
|
||||
onChange={(e) => setSelectedEventId(e.target.value)}
|
||||
>
|
||||
<option value="">General Notice</option>
|
||||
{events.map(ev => (
|
||||
<option key={ev.id} value={ev.id} style={{ color: ev.created_by !== currentUserId ? '#94a3b8' : 'inherit' }}>
|
||||
{ev.title} {ev.created_by !== currentUserId ? "(READ ONLY)" : ""}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{selectedEventId && selectedEventId !== 'GENERAL' && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDownloadExcel}
|
||||
className="w-14 h-14 bg-emerald-50 border border-emerald-100 text-emerald-600 rounded-xl flex items-center justify-center hover:bg-emerald-600 hover:text-white transition-all shadow-sm active:scale-95"
|
||||
title="Download Roster"
|
||||
>
|
||||
<i className="fas fa-file-csv"></i>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-[9px] font-black text-slate-500 uppercase tracking-[0.2em] mb-3 ml-1">Broadcast Type</label>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{(['DELAY', 'INFO', 'URGENT', 'ENDED', 'ONGOING'] as Announcement['type'][]).map(t => (
|
||||
<button
|
||||
key={t}
|
||||
type="button"
|
||||
onClick={() => setType(t)}
|
||||
className={`py-4 rounded-xl text-[9px] font-black uppercase tracking-widest transition-all border ${type === t ? 'bg-[#004a99] border-[#004a99] text-white shadow-md shadow-[#004a99]/20' : 'bg-slate-50 border-slate-200 text-slate-500 hover:text-slate-900 hover:border-slate-300'}`}
|
||||
>
|
||||
{t}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-2">
|
||||
<label className="block text-[9px] font-black text-slate-500 uppercase tracking-[0.2em] mb-4 ml-1">Auto-Expiry</label>
|
||||
<div className="grid grid-cols-3 gap-2 mb-4">
|
||||
{(['1H', 'NEVER', 'CUSTOM'] as ExpiryOption[]).map(opt => (
|
||||
<button
|
||||
key={opt}
|
||||
type="button"
|
||||
onClick={() => setExpiryOption(opt)}
|
||||
className={`py-3 rounded-xl text-[7px] font-black uppercase tracking-widest transition-all border ${expiryOption === opt ? 'bg-blue-50 border-blue-200 text-[#004a99]' : 'bg-transparent border-slate-200 text-slate-500 hover:bg-slate-50'}`}
|
||||
>
|
||||
{opt}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{expiryOption === 'CUSTOM' && (
|
||||
<input
|
||||
type="datetime-local"
|
||||
className="w-full bg-white border border-slate-200 rounded-xl px-5 py-4 text-slate-900 focus:ring-2 focus:ring-[#004a99] outline-none text-[10px] font-bold shadow-sm"
|
||||
value={customExpiry}
|
||||
onChange={(e) => setCustomExpiry(e.target.value)}
|
||||
required
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<textarea
|
||||
required
|
||||
placeholder="Broadcast message..."
|
||||
className="w-full bg-white border border-slate-200 rounded-xl px-6 py-5 text-slate-900 focus:ring-2 focus:ring-[#004a99] outline-none text-xs h-40 resize-none font-medium placeholder:text-slate-400 shadow-sm"
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
/>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className={`w-full py-5 rounded-xl font-black uppercase text-[10px] tracking-[0.3em] transition-all disabled:opacity-50 shadow-md hover:shadow-lg active:scale-95 ${editingId ? 'bg-blue-100 text-blue-800 hover:bg-blue-200 border border-blue-200' : 'bg-[#004a99] text-white hover:bg-blue-800'} `}
|
||||
>
|
||||
{isSubmitting ? <><i className="fas fa-spinner fa-spin mr-2"></i> SYNCING...</> : editingId ? 'UPDATE HISTORY' : 'PUBLISH NOTICE'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* Right Side: Live Stream History */}
|
||||
<div className="lg:col-span-8 bg-white border border-slate-200 rounded-[3.5rem] p-10 min-h-[700px] flex flex-col shadow-sm">
|
||||
<div className="flex items-center justify-between border-b border-slate-100 pb-10 mb-8">
|
||||
<div>
|
||||
<h3 className="text-2xl font-black uppercase tracking-tighter text-slate-900">Live Stream History</h3>
|
||||
<p className="text-[10px] font-black text-slate-500 uppercase tracking-widest mt-2">Real-time Authenticated Activity Feed</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="flex flex-col items-end">
|
||||
<span className="text-[10px] font-black text-[#004a99] uppercase tracking-widest">{announcements.length} ACTIVE BROADCASTS</span>
|
||||
<div className="w-12 h-0.5 bg-blue-200 mt-1"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 space-y-6 overflow-y-auto no-scrollbar pr-2 pb-10">
|
||||
{announcements.length > 0 ? announcements.map((ann) => (
|
||||
<div
|
||||
key={ann.id}
|
||||
className={`bg-white border rounded-[2.5rem] p-8 flex items-start justify-between group transition-all duration-500 hover:shadow-md ${editingId === ann.id ? 'border-blue-500 ring-1 ring-blue-500/20 bg-blue-50/50' : 'border-slate-200 hover:border-slate-300'
|
||||
}`}
|
||||
>
|
||||
<div className="flex-1 pr-10 text-left">
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
<span className={`px-4 py-1 rounded-full text-[9px] font-black uppercase tracking-widest border ${getStatusColor(ann.type)}`}>
|
||||
{ann.type}
|
||||
</span>
|
||||
<h4 className="text-base font-black text-slate-900 uppercase tracking-tight">{ann.title}</h4>
|
||||
</div>
|
||||
<p className="text-sm text-slate-600 font-medium leading-relaxed mb-8 max-w-2xl">{ann.message}</p>
|
||||
<div className="flex flex-wrap items-center gap-8">
|
||||
<div className="flex items-center gap-2 text-slate-500">
|
||||
<i className="far fa-clock text-[10px]"></i>
|
||||
<span className="text-[10px] font-black uppercase tracking-widest">
|
||||
Logged: {new Date(ann.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 self-center relative z-20">
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => { e.preventDefault(); e.stopPropagation(); handleEdit(ann); }}
|
||||
disabled={deletingId === ann.id}
|
||||
className="w-14 h-14 rounded-2xl bg-slate-50 flex items-center justify-center text-slate-400 hover:text-[#004a99] hover:bg-blue-50 transition-all border border-slate-200 hover:border-blue-200 disabled:opacity-30 cursor-pointer pointer-events-auto"
|
||||
>
|
||||
<i className="fas fa-pen-nib text-sm"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => { e.preventDefault(); e.stopPropagation(); setConfirmDeleteId(ann.id); }}
|
||||
disabled={deletingId === ann.id}
|
||||
className="w-14 h-14 rounded-2xl bg-slate-50 flex items-center justify-center text-slate-400 hover:text-rose-500 hover:bg-rose-50 transition-all border border-slate-200 hover:border-rose-200 disabled:opacity-30 cursor-pointer pointer-events-auto"
|
||||
>
|
||||
{deletingId === ann.id ? <i className="fas fa-spinner fa-spin text-sm"></i> : <i className="fas fa-trash-can text-sm"></i>}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)) : (
|
||||
<div className="flex flex-col items-center justify-center py-40 text-center animate-in fade-in duration-500 bg-slate-50 border border-dashed border-slate-200 rounded-[3rem]">
|
||||
<div className="w-24 h-24 rounded-full bg-white border border-slate-200 flex items-center justify-center mb-8 shadow-sm text-slate-300">
|
||||
<i className="fas fa-stream text-4xl"></i>
|
||||
</div>
|
||||
<h4 className="text-2xl font-black uppercase tracking-tighter text-slate-400">History Empty</h4>
|
||||
<p className="text-[10px] font-black text-slate-500 uppercase tracking-widest mt-3">Ready for local broadcast input</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminEventStatusView;
|
||||
1037
RIT-EVENT-MANAGEMENT--main/components/AdminLandingPage.tsx
Normal file
1037
RIT-EVENT-MANAGEMENT--main/components/AdminLandingPage.tsx
Normal file
File diff suppressed because it is too large
Load Diff
670
RIT-EVENT-MANAGEMENT--main/components/AdminStatusTrackerView.tsx
Normal file
670
RIT-EVENT-MANAGEMENT--main/components/AdminStatusTrackerView.tsx
Normal file
@@ -0,0 +1,670 @@
|
||||
import React, { useState, useMemo, useEffect } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { Event } from '../types';
|
||||
import { supabase } from '../supabase';
|
||||
|
||||
interface AdminStatusTrackerViewProps {
|
||||
events: Event[];
|
||||
onShowToast: (msg: string) => void;
|
||||
studentDocuments: Record<string, string>;
|
||||
onApproveCertificate?: (regId: string) => Promise<void>;
|
||||
localRegistrations?: any[];
|
||||
isAdmin?: boolean;
|
||||
currentUserId?: string;
|
||||
}
|
||||
|
||||
interface FilterState {
|
||||
depts: string[];
|
||||
years: string[];
|
||||
}
|
||||
|
||||
const AdminStatusTrackerView: React.FC<AdminStatusTrackerViewProps> = ({
|
||||
events,
|
||||
onShowToast,
|
||||
onApproveCertificate,
|
||||
localRegistrations = [],
|
||||
isAdmin = false,
|
||||
currentUserId
|
||||
}) => {
|
||||
const [selectedEventId, setSelectedEventId] = useState<string | null>(null);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [viewingDoc, setViewingDoc] = useState<string | null>(null);
|
||||
const [isApproving, setIsApproving] = useState<string | null>(null);
|
||||
const [liveRegistrations, setLiveRegistrations] = useState<any[]>(localRegistrations);
|
||||
const [isLoadingRegs, setIsLoadingRegs] = useState(true);
|
||||
|
||||
const [activeFilters, setActiveFilters] = useState<FilterState>({ depts: [], years: [] });
|
||||
const [showFilters, setShowFilters] = useState(false);
|
||||
const [viewMode, setViewMode] = useState<'ALL' | 'OD_PENDING' | 'CERT_PENDING'>('ALL');
|
||||
const [isUploadingOd, setIsUploadingOd] = useState<string | null>(null);
|
||||
const [externalUserIds, setExternalUserIds] = useState<Set<string>>(new Set());
|
||||
const [studentTypeFilter, setStudentTypeFilter] = useState<'INTERNAL' | 'EXTERNAL'>('INTERNAL');
|
||||
const [isLandingMode, setIsLandingMode] = useState(true);
|
||||
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
||||
const [isBulkUploading, setIsBulkUploading] = useState(false);
|
||||
|
||||
|
||||
const handleOdUpload = async (e: React.ChangeEvent<HTMLInputElement>, regId: string) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
setIsUploadingOd(regId);
|
||||
try {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = async () => {
|
||||
try {
|
||||
const base64 = reader.result as string;
|
||||
const isPdf = file.type === 'application/pdf';
|
||||
const extension = isPdf ? 'pdf' : 'jpg';
|
||||
const fileName = `OD_${regId}_${Date.now()}.${extension}`;
|
||||
|
||||
const { uploadToSupabase } = await import('../supabase');
|
||||
const publicUrl = await uploadToSupabase(base64, fileName, 'OD_PROVIDERS');
|
||||
|
||||
const { error } = await supabase.from('registrations').update({ od_url: publicUrl }).eq('id', regId);
|
||||
if (error) throw error;
|
||||
|
||||
onShowToast("OD Document uploaded successfully.");
|
||||
await refreshRegs();
|
||||
} catch (uploadErr: any) {
|
||||
console.error("OD Upload processing failed", uploadErr);
|
||||
alert(`OD Upload failed: ${uploadErr.message || 'Unknown error'}`);
|
||||
} finally {
|
||||
setIsUploadingOd(null);
|
||||
}
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
} catch (err: any) {
|
||||
console.error("OD Upload failed", err);
|
||||
alert(`OD Upload failed: ${err.message || 'Unknown error'}`);
|
||||
setIsUploadingOd(null);
|
||||
}
|
||||
};
|
||||
|
||||
// Fetch all registrations directly from Supabase (bypasses prop staleness)
|
||||
useEffect(() => {
|
||||
const fetchRegs = async () => {
|
||||
setIsLoadingRegs(true);
|
||||
const { data, error } = await supabase
|
||||
.from('registrations')
|
||||
.select('*')
|
||||
.order('registered_at', { ascending: false });
|
||||
if (data && !error) {
|
||||
setLiveRegistrations(data);
|
||||
} else if (localRegistrations.length > 0) {
|
||||
setLiveRegistrations(localRegistrations);
|
||||
}
|
||||
|
||||
const { data: extUsers } = await supabase.from('externalusers').select('id');
|
||||
if (extUsers) {
|
||||
setExternalUserIds(new Set(extUsers.map(u => u.id)));
|
||||
}
|
||||
|
||||
setIsLoadingRegs(false);
|
||||
};
|
||||
fetchRegs();
|
||||
}, []);
|
||||
|
||||
const refreshRegs = async () => {
|
||||
const { data, error } = await supabase
|
||||
.from('registrations')
|
||||
.select('*')
|
||||
.order('registered_at', { ascending: false });
|
||||
if (data && !error) setLiveRegistrations(data);
|
||||
|
||||
const { data: extUsers } = await supabase.from('externalusers').select('id');
|
||||
if (extUsers) {
|
||||
setExternalUserIds(new Set(extUsers.map(u => u.id)));
|
||||
}
|
||||
};
|
||||
|
||||
const handleApprove = async (regId: string) => {
|
||||
if (onApproveCertificate) {
|
||||
setIsApproving(regId);
|
||||
try {
|
||||
await onApproveCertificate(regId);
|
||||
onShowToast("Certificate verified and student hub updated.");
|
||||
await refreshRegs(); // Refresh so card instantly shows Verified
|
||||
} catch (err) {
|
||||
console.error("Approval failed:", err);
|
||||
} finally {
|
||||
setIsApproving(null);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownload = async (url: string, studentName: string, eventTitle: string) => {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
const blob = await response.blob();
|
||||
const blobUrl = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = blobUrl;
|
||||
link.download = `Cert_${studentName.replace(/\s+/g, '_')}_${eventTitle.replace(/\s+/g, '_')}.jpg`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(blobUrl);
|
||||
} catch (err) {
|
||||
console.error("Download failed:", err);
|
||||
}
|
||||
};
|
||||
|
||||
const filterOptions = {
|
||||
depts: ['CSBS', 'AIDS', 'AIML', 'ECE', 'VLSI', 'H&S', 'CCE', 'CSE', 'MECH', 'BIO-TECH', 'Information Technology (IT)', 'Electrical & Electronics Engineering (EEE)', 'Civil Engineering', 'Biomedical Engineering', 'Chemical Engineering', 'Aeronautical / Aerospace Engineering', 'Mechatronics Engineering', 'Others'],
|
||||
years: ['1st Year', '2nd Year', '3rd Year', '4th Year', '5th Year']
|
||||
};
|
||||
|
||||
const mappedParticipants = useMemo(() => {
|
||||
return liveRegistrations.map(reg => {
|
||||
const event = events.find(e => e.id === reg.event_id);
|
||||
const isEnded = event?.status === 'Completed';
|
||||
const isExternal = externalUserIds.has(reg.user_id);
|
||||
const isVerified = reg.certification_status === 'APPROVED' || isExternal;
|
||||
const hasUploaded = !!reg.certification_url;
|
||||
|
||||
const hasUploadedOd = !!reg.od_url;
|
||||
const isFree = event?.pricingType === 'FREE';
|
||||
const isPaidVerified = reg.payment_status === 'COMPLETED';
|
||||
|
||||
const stages = [
|
||||
{ label: 'Registered', status: 'completed' },
|
||||
{ label: 'TEAM', status: !event?.isTeamEvent || reg.team_code ? 'completed' : 'active' },
|
||||
{ label: 'Payment', status: isFree || isPaidVerified ? 'completed' : (reg.team_code || !event?.isTeamEvent ? 'active' : 'pending') },
|
||||
{ label: 'Ticket', status: isFree || isPaidVerified ? 'completed' : 'pending' },
|
||||
{ label: 'Ongoing', status: isEnded ? 'completed' : (event?.status === 'Event Ongoing' ? 'active' : 'pending') },
|
||||
{ label: 'Ended', status: isEnded ? 'completed' : 'pending' },
|
||||
{ label: 'Certification', status: isVerified ? 'completed' : (isEnded ? 'active' : 'pending') },
|
||||
{ label: 'OD', status: hasUploadedOd ? 'completed' : (isVerified ? 'active' : 'pending') }
|
||||
];
|
||||
|
||||
return {
|
||||
...reg,
|
||||
name: reg.user_name || 'Student',
|
||||
regNo: reg.reg_no || 'N/A',
|
||||
phone: reg.phone || 'No Phone',
|
||||
dept: reg.dept || 'N/A',
|
||||
year: reg.year || 'N/A',
|
||||
college: reg.college || null,
|
||||
eventTitle: event?.title || 'Unknown Event',
|
||||
stages,
|
||||
hasUploaded,
|
||||
isVerified,
|
||||
isExternal,
|
||||
hasUploadedOd,
|
||||
od_url: reg.od_url
|
||||
};
|
||||
});
|
||||
}, [liveRegistrations, events]);
|
||||
|
||||
const filteredParticipants = useMemo(() => {
|
||||
return mappedParticipants.filter(p => {
|
||||
const eventMatch = !selectedEventId || p.event_id === selectedEventId;
|
||||
const searchMatch = p.name.toLowerCase().includes(searchTerm.toLowerCase()) || p.regNo.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
const deptMatch = activeFilters.depts.length === 0 || activeFilters.depts.includes(p.dept);
|
||||
const yearMatch = activeFilters.years.length === 0 || activeFilters.years.includes(p.year);
|
||||
|
||||
let viewMatch = true;
|
||||
if (viewMode === 'OD_PENDING') viewMatch = p.isVerified;
|
||||
if (viewMode === 'CERT_PENDING') viewMatch = p.hasUploaded || (p.stages[5].status === 'completed'); // Ended or Uploaded
|
||||
|
||||
const isExternal = externalUserIds.has(p.user_id);
|
||||
const typeMatch = studentTypeFilter === 'EXTERNAL' ? isExternal : !isExternal;
|
||||
|
||||
return eventMatch && searchMatch && deptMatch && yearMatch && viewMatch && typeMatch;
|
||||
}).sort((a, b) => {
|
||||
// Sort pending certificates to the top
|
||||
if (a.hasUploaded && !a.isVerified && (!b.hasUploaded || b.isVerified)) return -1;
|
||||
if (b.hasUploaded && !b.isVerified && (!a.hasUploaded || a.isVerified)) return 1;
|
||||
return 0;
|
||||
});
|
||||
}, [mappedParticipants, selectedEventId, searchTerm, activeFilters, externalUserIds, studentTypeFilter, viewMode]);
|
||||
|
||||
const handleBulkOdUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file || selectedIds.size === 0) return;
|
||||
|
||||
setIsBulkUploading(true);
|
||||
try {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = async () => {
|
||||
try {
|
||||
const base64 = reader.result as string;
|
||||
const isPdf = file.type === 'application/pdf';
|
||||
const extension = isPdf ? 'pdf' : 'jpg';
|
||||
const fileName = `BULK_OD_${Date.now()}.${extension}`;
|
||||
|
||||
const { uploadToSupabase } = await import('../supabase');
|
||||
const publicUrl = await uploadToSupabase(base64, fileName, 'OD_PROVIDERS');
|
||||
|
||||
const idsArray = Array.from(selectedIds);
|
||||
const { error } = await supabase.from('registrations').update({ od_url: publicUrl }).in('id', idsArray);
|
||||
if (error) throw error;
|
||||
|
||||
onShowToast(`OD Document assigned to ${selectedIds.size} students successfully.`);
|
||||
setSelectedIds(new Set());
|
||||
await refreshRegs();
|
||||
} catch (uploadErr: any) {
|
||||
console.error("Bulk OD Upload failed", uploadErr);
|
||||
alert(`Bulk OD Upload failed: ${uploadErr.message || 'Unknown error'}`);
|
||||
} finally {
|
||||
setIsBulkUploading(false);
|
||||
}
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
} catch (err: any) {
|
||||
console.error("Bulk OD Upload failed", err);
|
||||
alert(`Bulk OD Upload failed: ${err.message || 'Unknown error'}`);
|
||||
setIsBulkUploading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleSelectAll = () => {
|
||||
if (selectedIds.size === filteredParticipants.length) {
|
||||
setSelectedIds(new Set());
|
||||
} else {
|
||||
setSelectedIds(new Set(filteredParticipants.map(p => p.id)));
|
||||
}
|
||||
};
|
||||
|
||||
const toggleFilterValue = (category: 'depts' | 'years', value: string) => {
|
||||
|
||||
setActiveFilters(prev => {
|
||||
const current = prev[category];
|
||||
const next = current.includes(value) ? current.filter(v => v !== value) : [...current, value];
|
||||
return { ...prev, [category]: next };
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="animate-in fade-in slide-in-from-bottom-10 duration-500 max-w-7xl mx-auto py-10 px-4">
|
||||
{isLandingMode ? (
|
||||
<div className="flex flex-col items-center py-20 px-4 text-center">
|
||||
<div className="mb-12">
|
||||
<h3 className="text-5xl font-black text-slate-900 tracking-tighter uppercase mb-3">
|
||||
Status <span className="text-[#004a99]">Tracker</span>
|
||||
</h3>
|
||||
<p className="text-slate-400 font-bold uppercase tracking-[0.4em] text-xs">Choose Verification Category</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 w-full max-w-5xl">
|
||||
<button
|
||||
onClick={() => { setViewMode('OD_PENDING'); setIsLandingMode(false); }}
|
||||
className="group relative h-[350px] bg-white border border-slate-200 rounded-[3rem] p-12 overflow-hidden transition-all duration-500 hover:shadow-2xl hover:border-[#004a99] hover:-translate-y-2 flex flex-col items-center justify-center text-center shadow-sm"
|
||||
>
|
||||
<div className="absolute top-0 right-0 p-8 opacity-10 group-hover:scale-110 transition-transform duration-700">
|
||||
<i className="fas fa-file-invoice-dollar text-[120px] text-[#004a99]"></i>
|
||||
</div>
|
||||
<div className="w-24 h-24 rounded-3xl bg-blue-50 flex items-center justify-center text-[#004a99] mb-8 group-hover:bg-[#004a99] group-hover:text-white transition-all duration-500 shadow-inner">
|
||||
<i className="fas fa-file-signature text-4xl"></i>
|
||||
</div>
|
||||
<h4 className="text-3xl font-black text-slate-900 uppercase tracking-tighter mb-4">OD Pending <span className="text-[#004a99]">List</span></h4>
|
||||
<p className="text-slate-500 font-medium leading-relaxed max-w-[240px] mb-8">Process on-duty requests and verify institutional approvals for students.</p>
|
||||
<div className="flex items-center gap-2 text-[#004a99] font-black text-xs uppercase tracking-[0.2em]">
|
||||
Explore Registry <i className="fas fa-arrow-right animate-pulse"></i>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => { setViewMode('CERT_PENDING'); setIsLandingMode(false); }}
|
||||
className="group relative h-[350px] bg-white border border-slate-200 rounded-[3rem] p-12 overflow-hidden transition-all duration-500 hover:shadow-2xl hover:border-orange-500 hover:-translate-y-2 flex flex-col items-center justify-center text-center shadow-sm"
|
||||
>
|
||||
<div className="absolute top-0 right-0 p-8 opacity-10 group-hover:scale-110 transition-transform duration-700">
|
||||
<i className="fas fa-certificate text-[120px] text-orange-500"></i>
|
||||
</div>
|
||||
<div className="w-24 h-24 rounded-3xl bg-orange-50 flex items-center justify-center text-orange-500 mb-8 group-hover:bg-orange-500 group-hover:text-white transition-all duration-500 shadow-inner">
|
||||
<i className="fas fa-award text-4xl"></i>
|
||||
</div>
|
||||
<h4 className="text-3xl font-black text-slate-900 uppercase tracking-tighter mb-4">Certifications <span className="text-orange-500">Pending</span></h4>
|
||||
<p className="text-slate-500 font-medium leading-relaxed max-w-[240px] mb-8">Verify event completion certificates and academic proof of participation.</p>
|
||||
<div className="flex items-center gap-2 text-orange-600 font-black text-xs uppercase tracking-[0.2em]">
|
||||
Explore Registry <i className="fas fa-arrow-right animate-pulse"></i>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => { setViewMode('ALL'); setIsLandingMode(false); }}
|
||||
className="mt-16 text-slate-400 hover:text-[#004a99] font-black text-[10px] uppercase tracking-[0.4em] transition-colors border-b-2 border-transparent hover:border-[#004a99] pb-1"
|
||||
>
|
||||
Or Manage All Participants <i className="fas fa-external-link-alt ml-2"></i>
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex flex-col md:flex-row md:items-end justify-between mb-12 gap-8 border-b border-slate-200 pb-8 relative">
|
||||
<button
|
||||
onClick={() => setIsLandingMode(true)}
|
||||
className="absolute -top-12 left-0 text-[10px] font-black text-slate-400 uppercase tracking-widest hover:text-[#004a99] transition-all flex items-center gap-2"
|
||||
>
|
||||
<i className="fas fa-chevron-left text-[8px]"></i> Return to Choices
|
||||
</button>
|
||||
<div>
|
||||
<h3 className="text-4xl font-black text-slate-900 tracking-tighter uppercase mb-2">
|
||||
{viewMode === 'OD_PENDING' ? 'OD' : (viewMode === 'CERT_PENDING' ? 'Certification' : 'System')} <span className="text-[#004a99] font-black">{viewMode === 'ALL' ? 'Monitor' : 'Queue'}</span>
|
||||
</h3>
|
||||
<p className="text-[10px] font-black text-slate-500 uppercase tracking-[0.4em]">
|
||||
Real-time Tracking & Verification Hub
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-4 flex-1 max-w-3xl items-center">
|
||||
<div className="relative flex-1 group w-full">
|
||||
<select
|
||||
className="w-full bg-white border border-slate-200 rounded-2xl pl-12 pr-6 py-4 text-slate-900 focus:ring-2 focus:ring-[#004a99] outline-none font-bold appearance-none cursor-pointer shadow-sm"
|
||||
value={selectedEventId || ''}
|
||||
onChange={(e) => setSelectedEventId(e.target.value || null)}
|
||||
>
|
||||
<option value="">All Contexts</option>
|
||||
{events.map(ev => <option key={ev.id} value={ev.id}>{ev.title}</option>)}
|
||||
</select>
|
||||
<i className="fas fa-layer-group absolute left-5 top-1/2 -translate-y-1/2 text-slate-400"></i>
|
||||
</div>
|
||||
|
||||
<div className="relative flex-[1.5] group flex gap-3 w-full">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search registry..."
|
||||
className="flex-1 bg-white border border-slate-200 rounded-2xl px-6 py-4 text-slate-900 focus:ring-2 focus:ring-[#004a99] outline-none font-bold placeholder:text-slate-400 shadow-sm"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
<button onClick={() => setShowFilters(true)} className="bg-white border border-slate-200 px-6 py-4 rounded-2xl hover:border-[#004a99] hover:text-[#004a99] text-slate-500 transition-all shadow-sm">
|
||||
<i className="fas fa-sliders-h"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{viewMode !== 'CERT_PENDING' && (
|
||||
<div className="flex gap-4 mb-8">
|
||||
<button
|
||||
onClick={() => setStudentTypeFilter('INTERNAL')}
|
||||
className={`flex-1 py-6 rounded-3xl border-2 transition-all flex flex-col items-center justify-center gap-2 ${studentTypeFilter === 'INTERNAL' ? 'bg-blue-50 border-[#004a99] text-[#004a99] shadow-md' : 'bg-white border-slate-200 text-slate-500 hover:border-blue-300'}`}
|
||||
>
|
||||
<i className="fas fa-university text-2xl"></i>
|
||||
<span className="font-black uppercase tracking-widest text-xs">Internal Students</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setStudentTypeFilter('EXTERNAL')}
|
||||
className={`flex-1 py-6 rounded-3xl border-2 transition-all flex flex-col items-center justify-center gap-2 ${studentTypeFilter === 'EXTERNAL' ? 'bg-orange-50 border-orange-500 text-orange-600 shadow-md' : 'bg-white border-slate-200 text-slate-500 hover:border-orange-300'}`}
|
||||
>
|
||||
<i className="fas fa-globe text-2xl"></i>
|
||||
<span className="font-black uppercase tracking-widest text-xs">External Students</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 gap-8">
|
||||
{isLoadingRegs ? (
|
||||
<div className="py-40 flex flex-col items-center justify-center text-center bg-white border border-dashed border-slate-200 rounded-[3rem]">
|
||||
<div className="w-16 h-16 border-4 border-blue-100 border-t-[#004a99] rounded-full animate-spin mb-6"></div>
|
||||
<p className="text-slate-500 font-black uppercase tracking-[0.3em] text-sm">Syncing Registrations...</p>
|
||||
</div>
|
||||
) : filteredParticipants.length > 0 ? filteredParticipants.map((p) => (
|
||||
<div key={p.id} className={`bg-white border ${selectedIds.has(p.id) ? 'border-[#004a99] ring-2 ring-blue-50' : 'border-slate-200'} rounded-[3rem] p-8 flex flex-col xl:flex-row items-center gap-10 hover:border-blue-300 transition-all duration-500 group/card shadow-sm hover:shadow-xl relative`}>
|
||||
{/* Selection Checkbox */}
|
||||
<div className="absolute top-8 left-8 z-20">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="w-5 h-5 rounded-lg border-2 border-slate-200 text-[#004a99] focus:ring-[#004a99] cursor-pointer"
|
||||
checked={selectedIds.has(p.id)}
|
||||
onChange={(e) => {
|
||||
const newSet = new Set(selectedIds);
|
||||
if (e.target.checked) newSet.add(p.id);
|
||||
else newSet.delete(p.id);
|
||||
setSelectedIds(newSet);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Student Info */}
|
||||
<div className="flex items-center gap-6 w-full xl:w-[24rem] text-left pl-10">
|
||||
|
||||
<div className="w-20 h-20 rounded-3xl bg-blue-50 border border-blue-100 flex items-center justify-center text-[#004a99] text-2xl font-black">
|
||||
{p.name.charAt(0)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-3 mb-1">
|
||||
<h4 className="text-xl font-black text-slate-900 uppercase tracking-tight truncate">{p.name}</h4>
|
||||
{p.hasUploaded && !p.isVerified && (
|
||||
<span className="bg-amber-100 text-amber-700 text-[7px] font-black px-2 py-0.5 rounded-full animate-pulse border border-amber-200">PENDING</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-[10px] font-black text-slate-500 uppercase tracking-widest leading-none mb-1">{p.regNo}</p>
|
||||
{p.team_name && (
|
||||
<p className="text-[9px] font-black text-blue-600 uppercase tracking-widest mb-1 flex items-center gap-1">
|
||||
<i className="fas fa-users"></i> {p.team_name}
|
||||
</p>
|
||||
)}
|
||||
<p className="text-[9px] font-bold text-slate-400 uppercase tracking-widest mb-1">{p.dept} • {p.year}</p>
|
||||
{p.college && (
|
||||
<p className="text-[9px] font-black text-orange-500 uppercase tracking-widest mb-2 flex items-center gap-1">
|
||||
<i className="fas fa-university"></i> {p.college}
|
||||
</p>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-emerald-500"></div>
|
||||
<p className="text-[9px] font-black text-slate-500 uppercase tracking-widest truncate">{p.eventTitle}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Timeline View */}
|
||||
<div className="flex-1 w-full overflow-x-auto no-scrollbar py-2">
|
||||
<div className="flex items-start justify-between min-w-[500px] relative px-6">
|
||||
<div className="absolute top-6 left-12 right-12 h-[2px] bg-slate-100 z-0"></div>
|
||||
{p.stages.map((stage: any, idx: number) => (
|
||||
<div key={idx} className="relative z-10 flex flex-col items-center w-16">
|
||||
<div className={`w-12 h-12 rounded-2xl flex items-center justify-center border transition-all duration-500 ${stage.status === 'completed' ? 'bg-[#004a99] border-blue-800 text-white shadow-lg shadow-blue-900/20' :
|
||||
stage.status === 'active' ? 'bg-white border-blue-200 text-[#004a99] shadow-sm' :
|
||||
'bg-slate-50 border-slate-200 text-slate-400'
|
||||
}`}>
|
||||
{stage.status === 'completed' ? <i className="fas fa-check text-sm"></i> : <span className="text-[10px] font-black">{idx + 1}</span>}
|
||||
</div>
|
||||
<span className="mt-4 text-[8px] font-black uppercase tracking-widest text-slate-500 text-center">{stage.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions Area */}
|
||||
<div className="w-full xl:w-64 flex flex-col gap-3">
|
||||
{p.certification_url && (
|
||||
<div className="flex gap-2">
|
||||
<button onClick={() => setViewingDoc(p.certification_url)} className="flex-1 py-4 bg-white border border-slate-200 text-slate-700 rounded-2xl text-[9px] font-black uppercase tracking-widest hover:border-[#004a99] hover:text-[#004a99] transition-all shadow-sm">
|
||||
<i className="fas fa-eye mr-2"></i> Inspect
|
||||
</button>
|
||||
<button onClick={() => handleDownload(p.certification_url, p.name, p.eventTitle)} className="w-14 py-4 bg-white border border-slate-200 text-slate-500 rounded-2xl hover:text-[#004a99] hover:border-blue-200 transition-all shadow-sm">
|
||||
<i className="fas fa-download"></i>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Certification Status / Approval */}
|
||||
{(() => {
|
||||
const eventObj = events.find(e => e.id === p.event_id);
|
||||
const isOwner = eventObj && eventObj.created_by === currentUserId;
|
||||
|
||||
if (p.isExternal) {
|
||||
return (
|
||||
<div className="w-full py-3 bg-blue-50 border border-blue-100 text-blue-600 rounded-2xl text-[10px] font-black uppercase tracking-widest text-center flex items-center justify-center gap-2 shadow-sm">
|
||||
<i className="fas fa-check-circle"></i> Auto-Verified (External)
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (p.hasUploaded && !p.isVerified) {
|
||||
if (isAdmin || !isOwner) {
|
||||
return (
|
||||
<div className="w-full py-3 bg-emerald-50 border border-emerald-100 text-emerald-600 rounded-2xl text-[10px] font-black uppercase tracking-widest text-center flex items-center justify-center gap-2 shadow-sm opacity-60">
|
||||
<i className="fas fa-shield-check"></i> {isOwner ? "Pending Approval" : "Read Only (Other Coord)"}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<button
|
||||
onClick={() => handleApprove(p.id)}
|
||||
disabled={isApproving === p.id}
|
||||
className="w-full py-3 bg-emerald-600 text-white rounded-2xl text-[10px] font-black uppercase tracking-widest hover:bg-emerald-700 transition-all shadow-lg shadow-emerald-600/20 active:scale-95 disabled:opacity-50"
|
||||
>
|
||||
{isApproving === p.id ? <><i className="fas fa-spinner fa-spin mr-2"></i> Syncing</> : <><i className="fas fa-check-double mr-2"></i> Approve Proof</>}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
if (p.isVerified) {
|
||||
return (
|
||||
<div className="w-full py-3 bg-emerald-50 border border-emerald-100 text-emerald-600 rounded-2xl text-[10px] font-black uppercase tracking-widest text-center flex items-center justify-center gap-2 shadow-sm">
|
||||
<i className="fas fa-shield-check"></i> Verified Credentials
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full py-3 bg-slate-50 border border-slate-200 text-slate-500 rounded-2xl text-[9px] font-black uppercase tracking-widest text-center flex items-center justify-center gap-2">
|
||||
<i className="fas fa-clock"></i> Awaiting Cert Upload
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* OD Upload Actions */}
|
||||
{(() => {
|
||||
const eventObj = events.find(e => e.id === p.event_id);
|
||||
const isOwner = eventObj && eventObj.created_by === currentUserId;
|
||||
|
||||
if (!p.hasUploadedOd) {
|
||||
if (isAdmin || !isOwner) {
|
||||
return (
|
||||
<div className="w-full py-3 bg-blue-50 border border-blue-100 text-[#004a99] rounded-2xl text-[10px] font-black uppercase tracking-widest text-center flex items-center justify-center gap-2 shadow-sm opacity-60">
|
||||
<i className="fas fa-file"></i> {isOwner ? "No OD Uploaded" : "Read Only"}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<label className={`w-full py-3 ${isUploadingOd === p.id || p.hasUploadedOd ? 'bg-blue-200 text-blue-500 cursor-not-allowed' : 'bg-blue-50 border border-blue-200 text-[#004a99] cursor-pointer hover:bg-blue-100'} rounded-2xl text-[10px] font-black uppercase tracking-widest text-center transition-all flex justify-center items-center gap-2`}>
|
||||
{isUploadingOd === p.id ? <><i className="fas fa-spinner fa-spin"></i> Uploading OD...</> : <><i className="fas fa-upload"></i> Upload OD</>}
|
||||
<input type="file" className="hidden" accept="image/*,.pdf" onChange={(e) => handleOdUpload(e, p.id)} disabled={isUploadingOd === p.id || p.hasUploadedOd} />
|
||||
</label>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="w-full py-3 bg-teal-50 border border-teal-100 text-teal-600 rounded-2xl text-[10px] font-black uppercase tracking-widest text-center flex items-center justify-center gap-2 shadow-sm">
|
||||
<i className="fas fa-check-circle"></i> OD Uploaded
|
||||
<button onClick={() => setViewingDoc(p.od_url)} className="ml-2 hover:text-teal-800 focus:outline-none"><i className="fas fa-eye"></i></button>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
)) : (
|
||||
<div className="py-40 flex flex-col items-center justify-center text-center bg-white border-2 border-dashed border-slate-200 rounded-[4rem]">
|
||||
<div className="w-20 h-20 bg-slate-50 rounded-full flex items-center justify-center mb-8">
|
||||
<i className="fas fa-database text-slate-300 text-3xl"></i>
|
||||
</div>
|
||||
<h4 className="text-2xl font-black uppercase tracking-tighter text-slate-400">No matching student data</h4>
|
||||
<p className="text-[10px] font-black text-slate-500 uppercase tracking-widest mt-3">Try adjusting your filters or search term</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Floating Bulk Action Bar */}
|
||||
{selectedIds.size > 0 && (
|
||||
<div className="fixed bottom-10 left-1/2 -translate-x-1/2 z-[1000] flex justify-center animate-in slide-in-from-bottom-10 pointer-events-none">
|
||||
<div className="bg-white/95 backdrop-blur-xl border border-slate-200 shadow-2xl rounded-full px-10 py-5 flex items-center gap-8 pointer-events-auto ring-1 ring-[#004a99]/10">
|
||||
<div className="flex items-center gap-4 border-r border-slate-100 pr-8">
|
||||
<button
|
||||
onClick={toggleSelectAll}
|
||||
className="w-10 h-10 rounded-xl bg-slate-50 flex items-center justify-center text-[#004a99] hover:bg-blue-50 transition-all border border-slate-100"
|
||||
>
|
||||
<i className={`fas ${selectedIds.size === filteredParticipants.length ? 'fa-check-double' : 'fa-square'}`}></i>
|
||||
</button>
|
||||
<span className="text-xs font-black text-slate-900 uppercase tracking-widest whitespace-nowrap">
|
||||
{selectedIds.size} Students Active
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<label className={`bg-[#004a99] text-white px-10 py-3.5 rounded-full text-[10px] font-black uppercase tracking-widest hover:bg-blue-800 transition-all shadow-xl shadow-blue-900/20 active:scale-95 flex items-center gap-3 cursor-pointer ${isBulkUploading ? 'opacity-50 pointer-events-none' : ''}`}>
|
||||
{isBulkUploading ? <i className="fas fa-spinner fa-spin"></i> : <i className="fas fa-file-export"></i>}
|
||||
{isBulkUploading ? "Processing..." : "Provide Bulk OD"}
|
||||
<input type="file" className="hidden" accept="image/*,.pdf" onChange={handleBulkOdUpload} disabled={isBulkUploading} />
|
||||
</label>
|
||||
|
||||
<button
|
||||
onClick={() => setSelectedIds(new Set())}
|
||||
className="text-[10px] font-black text-slate-400 hover:text-rose-500 uppercase tracking-widest transition-colors px-4"
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showFilters && createPortal(
|
||||
|
||||
<div className="fixed inset-0 z-[10005] flex justify-end">
|
||||
<div className="absolute inset-0 bg-slate-900/40 backdrop-blur-sm animate-in fade-in duration-300" onClick={() => setShowFilters(false)} />
|
||||
<div className="relative w-full max-w-md bg-white h-full shadow-2xl flex flex-col animate-in slide-in-from-right duration-500 border-l border-slate-200">
|
||||
<div className="p-10 border-b border-slate-100 flex justify-between items-center bg-slate-50/50">
|
||||
<div>
|
||||
<h4 className="text-2xl font-black text-slate-900 uppercase tracking-tight">System Filters</h4>
|
||||
<p className="text-[10px] font-black text-[#004a99] uppercase tracking-widest mt-1">Refine Registry View</p>
|
||||
</div>
|
||||
<button onClick={() => setShowFilters(false)} className="w-12 h-12 rounded-full bg-white border border-slate-200 flex items-center justify-center text-slate-400 hover:text-slate-900 hover:border-slate-300 transition-all shadow-sm"><i className="fas fa-times"></i></button>
|
||||
</div>
|
||||
<div className="flex-1 p-10 space-y-4 overflow-y-auto no-scrollbar">
|
||||
<h5 className="text-[10px] font-black text-slate-500 uppercase tracking-[0.3em] mb-6">Departments</h5>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{filterOptions.depts.map(dept => (
|
||||
<button
|
||||
key={dept}
|
||||
onClick={() => toggleFilterValue('depts', dept)}
|
||||
className={`py-3 rounded-xl text-[9px] font-black uppercase transition-all border ${activeFilters.depts.includes(dept) ? 'bg-[#004a99] border-[#004a99] text-white shadow-md shadow-blue-900/20' : 'bg-white border-slate-200 text-slate-500 hover:border-slate-300'}`}
|
||||
>
|
||||
{dept}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<h5 className="text-[10px] font-black text-slate-500 uppercase tracking-[0.3em] mb-6 mt-8">Years</h5>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{filterOptions.years.map(year => (
|
||||
<button
|
||||
key={year}
|
||||
onClick={() => toggleFilterValue('years', year)}
|
||||
className={`py-3 rounded-xl text-[9px] font-black uppercase transition-all border ${activeFilters.years.includes(year) ? 'bg-[#004a99] border-[#004a99] text-white shadow-md shadow-blue-900/20' : 'bg-white border-slate-200 text-slate-500 hover:border-slate-300'}`}
|
||||
>
|
||||
{year}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-10 bg-slate-50 border-t border-slate-200 flex gap-4">
|
||||
<button onClick={() => setActiveFilters({ depts: [], years: [] })} className="flex-1 py-4 bg-white border border-slate-200 rounded-2xl text-[10px] font-black text-slate-600 uppercase tracking-widest hover:bg-slate-50 transition-all shadow-sm">Reset View</button>
|
||||
<button onClick={() => setShowFilters(false)} className="flex-1 py-4 bg-[#004a99] text-white rounded-2xl text-[10px] font-black uppercase tracking-widest hover:bg-blue-900 shadow-xl shadow-blue-900/20 active:scale-95 transition-all">Apply Filters</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
)}
|
||||
|
||||
{viewingDoc && createPortal(
|
||||
<div className="fixed inset-0 z-[10005] flex items-center justify-center bg-slate-900/60 backdrop-blur-md p-4 animate-in fade-in duration-300" onClick={() => setViewingDoc(null)}>
|
||||
<div className="relative w-full max-w-4xl max-h-[90vh] bg-white rounded-[3rem] overflow-hidden shadow-2xl animate-in zoom-in-95 duration-500" onClick={e => e.stopPropagation()}>
|
||||
<div className="p-6 border-b border-slate-100 flex justify-between items-center bg-slate-50/50">
|
||||
<span className="text-[10px] font-black text-slate-500 uppercase tracking-[0.4em]">Proof Inspection Mode</span>
|
||||
<button onClick={() => setViewingDoc(null)} className="w-10 h-10 rounded-full bg-white border border-slate-200 flex items-center justify-center text-slate-500 hover:text-rose-600 hover:border-rose-200 transition-all shadow-sm"><i className="fas fa-times"></i></button>
|
||||
</div>
|
||||
<div className="p-10 flex items-center justify-center min-h-[50vh] bg-slate-100/50">
|
||||
<img src={viewingDoc} className="max-w-full max-h-[70vh] object-contain rounded-2xl shadow-lg ring-1 ring-slate-200" alt="Document" />
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminStatusTrackerView;
|
||||
106
RIT-EVENT-MANAGEMENT--main/components/CategoryGrid.tsx
Normal file
106
RIT-EVENT-MANAGEMENT--main/components/CategoryGrid.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
|
||||
import React from 'react';
|
||||
import { CATEGORIES } from '../constants';
|
||||
|
||||
interface CategoryGridProps {
|
||||
onSelectCategory: (id: string) => void;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
const CategoryGrid: React.FC<CategoryGridProps> = ({ onSelectCategory }) => {
|
||||
const threeCategories = CATEGORIES.filter(c => c.id !== 'CENTRE-ACTIVITY');
|
||||
const centreActivityCategory = CATEGORIES.find(c => c.id === 'CENTRE-ACTIVITY');
|
||||
|
||||
return (
|
||||
<section className="py-12 px-6 md:px-12 lg:px-24 bg-white animate-in fade-in slide-in-from-bottom-4 duration-500">
|
||||
<h2 className="text-3xl md:text-4xl font-black text-center text-[#1A202C] mb-12 tracking-tight uppercase">
|
||||
Discover by <span className="text-[#f97316]">Category</span>
|
||||
</h2>
|
||||
|
||||
{/* Three Standard Categories */}
|
||||
<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}
|
||||
onClick={() => onSelectCategory(cat.id)}
|
||||
className="group relative h-[380px] md:h-[500px] rounded-[3rem] overflow-hidden cursor-pointer shadow-[0_20px_50px_rgba(0,0,0,0.1)] transition-all duration-700 hover:scale-[1.02] hover:shadow-[0_40px_80px_rgba(0,0,0,0.25)]"
|
||||
>
|
||||
{/* Background Image with Zoom Effect */}
|
||||
<img
|
||||
src={cat.image}
|
||||
alt={cat.name}
|
||||
className="w-full h-full object-cover transition-transform duration-[1.5s] ease-out group-hover:scale-110"
|
||||
/>
|
||||
|
||||
{/* Premium Multi-layer Overlay */}
|
||||
<div className="absolute inset-0 bg-black/20 group-hover:bg-black/10 transition-colors duration-500"></div>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/90 via-black/40 to-transparent opacity-90 transition-opacity duration-500"></div>
|
||||
|
||||
{/* Border Glow on Hover */}
|
||||
<div className="absolute inset-0 border-[3px] border-white/0 group-hover:border-white/20 rounded-[3rem] transition-all duration-500 scale-95 group-hover:scale-100"></div>
|
||||
|
||||
{/* Rich Typography and Content */}
|
||||
<div className="absolute bottom-10 left-10 right-10 flex items-end justify-between">
|
||||
<div>
|
||||
<span className="inline-block px-4 py-1.5 bg-white/10 backdrop-blur-md rounded-full text-white/70 text-[10px] uppercase font-black tracking-[0.2em] mb-3 opacity-0 group-hover:opacity-100 transition-all duration-500 translate-y-4 group-hover:translate-y-0">
|
||||
Explore Events
|
||||
</span>
|
||||
<h3 className="text-3xl md:text-4xl font-black text-white tracking-tighter leading-none mb-1 shadow-black/20">
|
||||
{cat.name}
|
||||
</h3>
|
||||
<div className="h-1 w-12 bg-[#f97316] rounded-full transition-all duration-500 group-hover:w-24"></div>
|
||||
</div>
|
||||
|
||||
<div className="w-14 h-14 bg-white/10 backdrop-blur-md rounded-2xl flex items-center justify-center text-white text-xl transform translate-x-12 opacity-0 group-hover:translate-x-0 group-hover:opacity-100 transition-all duration-500 delay-100">
|
||||
<i className="fas fa-arrow-right"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Centre Activity Wide Card */}
|
||||
{centreActivityCategory && (
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div
|
||||
onClick={() => onSelectCategory(centreActivityCategory.id)}
|
||||
className="group relative h-[320px] md:h-[420px] rounded-[3.5rem] overflow-hidden cursor-pointer shadow-[0_20px_50px_rgba(0,0,0,0.15)] transition-all duration-700 hover:scale-[1.01] hover:shadow-[0_40px_80px_rgba(0,0,0,0.3)] border-[3px] border-transparent hover:border-white/20"
|
||||
>
|
||||
{/* Background Image with Zoom Effect */}
|
||||
<img
|
||||
src={centreActivityCategory.image}
|
||||
alt={centreActivityCategory.name}
|
||||
className="w-full h-full object-cover transition-transform duration-[1.5s] ease-out group-hover:scale-105"
|
||||
/>
|
||||
|
||||
{/* Overlays */}
|
||||
<div className="absolute inset-0 bg-black/45 group-hover:bg-black/35 transition-colors duration-500"></div>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-black/95 via-black/60 to-transparent"></div>
|
||||
|
||||
{/* Rich Typography and Content */}
|
||||
<div className="absolute inset-0 flex items-center justify-between px-10 md:px-20">
|
||||
<div className="space-y-4 text-left">
|
||||
<span className="inline-block px-5 py-2 bg-white/10 backdrop-blur-md rounded-full text-white/95 text-[10px] uppercase font-black tracking-[0.25em] opacity-0 group-hover:opacity-100 transition-all duration-500 translate-y-2 group-hover:translate-y-0">
|
||||
Special Hub Activities
|
||||
</span>
|
||||
<h3 className="text-4xl md:text-6xl font-black text-white tracking-tighter leading-none shadow-black/20 uppercase">
|
||||
{centreActivityCategory.name}
|
||||
</h3>
|
||||
<p className="text-white/80 font-medium tracking-wide text-xs md:text-sm max-w-2xl leading-relaxed">
|
||||
Engage in multidisciplinary innovation, advanced research hubs, incubation cells, and collaborative special projects driving academic excellence.
|
||||
</p>
|
||||
<div className="h-1.5 w-20 bg-[#f97316] rounded-full transition-all duration-500 group-hover:w-40"></div>
|
||||
</div>
|
||||
|
||||
<div className="w-16 h-16 md:w-20 md:h-20 bg-white/10 backdrop-blur-md rounded-3xl flex items-center justify-center text-white text-2xl md:text-3xl transform translate-x-12 opacity-0 group-hover:translate-x-0 group-hover:opacity-100 transition-all duration-500 delay-100 mr-4 shrink-0">
|
||||
<i className="fas fa-arrow-right"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default CategoryGrid;
|
||||
113
RIT-EVENT-MANAGEMENT--main/components/ContactSection.tsx
Normal file
113
RIT-EVENT-MANAGEMENT--main/components/ContactSection.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
import React from 'react';
|
||||
import { MapPin, Phone, Mail, Building } from 'lucide-react';
|
||||
|
||||
const ContactSection: React.FC = () => {
|
||||
return (
|
||||
<section className="py-24 px-6 md:px-12 lg:px-24 bg-white min-h-screen">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="mb-16 text-center">
|
||||
<h3 className="text-[#f97316] font-bold tracking-widest uppercase text-xl mb-2 relative inline-block">
|
||||
CONTACT US
|
||||
<span className="absolute -bottom-2 left-0 w-full h-1 bg-[#f97316]"></span>
|
||||
</h3>
|
||||
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold text-[#1e3a8a] mt-6">
|
||||
Get in Touch
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-start">
|
||||
{/* Contact Info */}
|
||||
<div className="space-y-12">
|
||||
{/* College Address */}
|
||||
<div className="bg-gray-50 p-8 rounded-2xl shadow-sm border border-gray-100 hover:shadow-md transition-shadow">
|
||||
<div className="flex items-center gap-4 mb-6">
|
||||
<div className="w-12 h-12 bg-blue-100 rounded-full flex items-center justify-center text-[#1e3a8a]">
|
||||
<Building size={24} />
|
||||
</div>
|
||||
<h3 className="text-2xl font-bold text-[#1e3a8a]">College</h3>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 text-gray-700">
|
||||
<div className="flex items-start gap-3">
|
||||
<MapPin size={20} className="text-[#f97316] mt-1 shrink-0" />
|
||||
<p className="leading-relaxed">
|
||||
Rajalakshmi Institute of Technology,<br />
|
||||
Poonamallee, Chennai - 600 124.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-3">
|
||||
<Phone size={20} className="text-[#f97316] mt-1 shrink-0" />
|
||||
<p>
|
||||
91 44 6718 1600 / 91 44 6718 1601 /<br />
|
||||
8925977445
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-3">
|
||||
<Mail size={20} className="text-[#f97316] mt-1 shrink-0" />
|
||||
<a href="mailto:mail@ritchennai.edu.in" className="hover:text-[#f97316] transition-colors">
|
||||
mail@ritchennai.edu.in
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Administrative Office */}
|
||||
<div className="bg-gray-50 p-8 rounded-2xl shadow-sm border border-gray-100 hover:shadow-md transition-shadow">
|
||||
<div className="flex items-center gap-4 mb-6">
|
||||
<div className="w-12 h-12 bg-orange-100 rounded-full flex items-center justify-center text-[#f97316]">
|
||||
<Building size={24} />
|
||||
</div>
|
||||
<h3 className="text-2xl font-bold text-[#1e3a8a]">Administrative Office</h3>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 text-gray-700">
|
||||
<div className="flex items-start gap-3">
|
||||
<MapPin size={20} className="text-[#f97316] mt-1 shrink-0" />
|
||||
<p className="leading-relaxed">
|
||||
#69, New Avadi Road, Kilpauk,<br />
|
||||
Chennai - 600 010.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-3">
|
||||
<Phone size={20} className="text-[#f97316] mt-1 shrink-0" />
|
||||
<div className="space-y-1">
|
||||
<p>Tel : 91 44 2644 2472</p>
|
||||
<p>91 44 2646 1316</p>
|
||||
<p>91 44 2646 0124</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-3">
|
||||
<Mail size={20} className="text-[#f97316] mt-1 shrink-0" />
|
||||
<a href="mailto:mail@ritchennai.edu.in" className="hover:text-[#f97316] transition-colors">
|
||||
mail@ritchennai.edu.in
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Map */}
|
||||
<div className="h-full min-h-[500px] rounded-2xl overflow-hidden shadow-lg border border-gray-200">
|
||||
<iframe
|
||||
width="100%"
|
||||
height="100%"
|
||||
frameBorder="0"
|
||||
scrolling="no"
|
||||
marginHeight={0}
|
||||
marginWidth={0}
|
||||
src="https://maps.google.com/maps?q=Rajalakshmi%20Institute%20of%20Technology&t=&z=13&ie=UTF8&iwloc=&output=embed"
|
||||
title="Rajalakshmi Institute of Technology Map"
|
||||
className="w-full h-full"
|
||||
></iframe>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContactSection;
|
||||
142
RIT-EVENT-MANAGEMENT--main/components/CreateDomainView.tsx
Normal file
142
RIT-EVENT-MANAGEMENT--main/components/CreateDomainView.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
import React, { useState } from 'react';
|
||||
import { supabase, uploadImageToSupabase } from '../supabase';
|
||||
|
||||
interface CreateDomainViewProps {
|
||||
onShowToast: (msg: string, type: 'success' | 'delete') => void;
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
const CreateDomainView: React.FC<CreateDomainViewProps> = ({ onShowToast, onBack }) => {
|
||||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
description: '',
|
||||
category: 'TECHNICAL',
|
||||
});
|
||||
const [imageFile, setImageFile] = useState<File | null>(null);
|
||||
const [imagePreview, setImagePreview] = useState<string | null>(null);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData(prev => ({ ...prev, [name]: value }));
|
||||
};
|
||||
|
||||
const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
if (file.size > 2 * 1024 * 1024) {
|
||||
alert("Image must be less than 2MB");
|
||||
return;
|
||||
}
|
||||
setImageFile(file);
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
setImagePreview(reader.result as string);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!imagePreview) {
|
||||
alert("Please upload a cover image for the domain.");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
if (!user) throw new Error("Authentication required");
|
||||
|
||||
const imagePath = `domain_covers/${Date.now()}_${formData.name.replace(/\s+/g, '_')}`;
|
||||
const imageUrl = await uploadImageToSupabase(imagePreview, imagePath, 'domains');
|
||||
|
||||
const { error } = await supabase.from('domains').insert({
|
||||
name: formData.name,
|
||||
description: formData.description,
|
||||
category: formData.category,
|
||||
image: imageUrl,
|
||||
status: formData.category === 'CENTRE-ACTIVITY' ? 'APPROVED' : 'PENDING',
|
||||
created_by: user.id
|
||||
});
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
onShowToast("Domain verification requested successfully!", "success");
|
||||
onBack();
|
||||
} catch (error: any) {
|
||||
console.error("Domain Request Error:", error);
|
||||
alert(`Request failed: ${error.message}`);
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-4xl mx-auto bg-white rounded-[3rem] p-8 md:p-12 shadow-[0_20px_50px_rgba(0,0,0,0.05)] border border-slate-100">
|
||||
<div className="flex items-center justify-between mb-10">
|
||||
<div>
|
||||
<h2 className="text-4xl font-black text-slate-900 uppercase tracking-tighter">Propose Domain</h2>
|
||||
<p className="text-slate-400 font-bold uppercase tracking-widest text-[10px] mt-1">Submit a new domain for verification</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onBack}
|
||||
className="w-12 h-12 bg-slate-50 text-slate-400 rounded-full flex items-center justify-center hover:bg-slate-100 hover:text-slate-600 transition-all border border-slate-200"
|
||||
>
|
||||
<i className="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-8">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<label className="block text-[10px] font-black text-slate-400 uppercase tracking-widest mb-3 ml-2">Domain Name *</label>
|
||||
<input required name="name" placeholder="e.g. Artificial Intelligence" className="w-full bg-slate-50 border-none rounded-2xl px-6 py-4 text-xs font-bold outline-none focus:ring-2 focus:ring-[#004a99] transition-all" value={formData.name} onChange={handleInputChange} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-[10px] font-black text-slate-400 uppercase tracking-widest mb-3 ml-2">Category *</label>
|
||||
<select required name="category" className="w-full bg-slate-50 border-none rounded-2xl px-6 py-4 text-xs font-bold outline-none focus:ring-2 focus:ring-[#004a99] transition-all" value={formData.category} onChange={handleInputChange}>
|
||||
<option value="TECHNICAL">Technical</option>
|
||||
<option value="NON-TECHNICAL">Non-Technical</option>
|
||||
<option value="WORKSHOP">Workshop</option>
|
||||
<option value="CENTRE-ACTIVITY">Centre Activity</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-[10px] font-black text-slate-400 uppercase tracking-widest mb-3 ml-2">Description</label>
|
||||
<textarea name="description" placeholder="Brief description of the domain..." rows={4} className="w-full bg-slate-50 border-none rounded-3xl px-6 py-5 text-xs font-medium outline-none focus:ring-2 focus:ring-[#004a99] transition-all resize-none" value={formData.description} onChange={handleInputChange}></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-[10px] font-black text-slate-400 uppercase tracking-widest mb-3 ml-2">Cover Image *</label>
|
||||
<div className="relative w-full h-64 bg-slate-50 border-2 border-dashed border-slate-200 rounded-[2rem] overflow-hidden group hover:border-[#004a99] transition-colors cursor-pointer flex flex-col items-center justify-center">
|
||||
<input type="file" accept="image/*" onChange={handleImageChange} className="absolute inset-0 w-full h-full opacity-0 cursor-pointer z-10" required />
|
||||
{imagePreview ? (
|
||||
<img src={imagePreview} alt="Preview" className="w-full h-full object-cover" />
|
||||
) : (
|
||||
<div className="text-center p-6">
|
||||
<div className="w-16 h-16 bg-white rounded-full flex items-center justify-center mx-auto mb-4 shadow-sm text-slate-300 group-hover:text-[#004a99] transition-colors">
|
||||
<i className="fas fa-image text-xl"></i>
|
||||
</div>
|
||||
<span className="block text-xs font-black text-slate-500 uppercase tracking-widest">Upload Cover</span>
|
||||
<span className="block text-[10px] font-medium text-slate-400 mt-2">Max. 2MB (16:9 Recommended)</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" disabled={isSubmitting} className="w-full py-5 bg-orange-500 text-white rounded-2xl font-black uppercase text-xs tracking-[0.2em] hover:bg-orange-600 transition-all shadow-xl shadow-orange-500/20 active:scale-95 disabled:opacity-70 disabled:cursor-not-allowed">
|
||||
{isSubmitting ? 'Submitting Request...' : 'Submit Domain for Verification'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateDomainView;
|
||||
1083
RIT-EVENT-MANAGEMENT--main/components/CreateEventForm.tsx
Normal file
1083
RIT-EVENT-MANAGEMENT--main/components/CreateEventForm.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,284 @@
|
||||
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { supabase } from '../supabase';
|
||||
import { SpecialEvent } from '../types';
|
||||
|
||||
interface CreateSpecialEventsViewProps {
|
||||
onShowToast: (msg: string, type: 'success' | 'delete') => void;
|
||||
}
|
||||
|
||||
const CreateSpecialEventsView: React.FC<CreateSpecialEventsViewProps> = ({ onShowToast }) => {
|
||||
const [specialEvents, setSpecialEvents] = useState<SpecialEvent[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [editingEvent, setEditingEvent] = useState<SpecialEvent | null>(null);
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
title: '',
|
||||
description: '',
|
||||
link: ''
|
||||
});
|
||||
|
||||
const fetchSpecialEvents = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
const { data, error } = await supabase
|
||||
.from('special_events')
|
||||
.select('*')
|
||||
.order('created_at', { ascending: false });
|
||||
|
||||
if (!error && data) {
|
||||
setSpecialEvents(data);
|
||||
}
|
||||
setIsLoading(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchSpecialEvents();
|
||||
}, [fetchSpecialEvents]);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setIsSubmitting(true);
|
||||
|
||||
try {
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
if (!user) throw new Error("User not authenticated");
|
||||
|
||||
if (editingEvent) {
|
||||
const { error } = await supabase
|
||||
.from('special_events')
|
||||
.update({
|
||||
title: formData.title,
|
||||
description: formData.description,
|
||||
link: formData.link
|
||||
})
|
||||
.eq('id', editingEvent.id);
|
||||
|
||||
if (error) throw error;
|
||||
onShowToast("Session updated! Awaiting Review.", "success");
|
||||
} else {
|
||||
const { error } = await supabase
|
||||
.from('special_events')
|
||||
.insert({
|
||||
title: formData.title,
|
||||
description: formData.description,
|
||||
link: formData.link,
|
||||
created_by: user.id,
|
||||
verification_status: 'PENDING'
|
||||
});
|
||||
|
||||
if (error) throw error;
|
||||
onShowToast("Session Broadcasted! Security check initiated.", "success");
|
||||
}
|
||||
|
||||
setFormData({ title: '', description: '', link: '' });
|
||||
setEditingEvent(null);
|
||||
fetchSpecialEvents();
|
||||
} catch (err: any) {
|
||||
console.error("Error saving special event:", err);
|
||||
alert(`Error: ${err.message}`);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = (event: SpecialEvent) => {
|
||||
setEditingEvent(event);
|
||||
setFormData({
|
||||
title: event.title,
|
||||
description: event.description || '',
|
||||
link: event.link
|
||||
});
|
||||
// Scroll to form
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
};
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
if (!confirm("Are you sure you want to delete this special event?")) return;
|
||||
|
||||
try {
|
||||
const { error } = await supabase
|
||||
.from('special_events')
|
||||
.delete()
|
||||
.eq('id', id);
|
||||
|
||||
if (error) throw error;
|
||||
onShowToast("Special event deleted successfully!", "delete");
|
||||
fetchSpecialEvents();
|
||||
} catch (err: any) {
|
||||
console.error("Error deleting special event:", err);
|
||||
alert(`Error: ${err.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto px-4 md:px-10 py-10 animate-in fade-in duration-700">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-12">
|
||||
{/* Creation Form */}
|
||||
<div className="lg:col-span-1">
|
||||
<div className="bg-white border border-slate-200 rounded-[2.5rem] p-10 shadow-xl sticky top-32">
|
||||
<h2 className="text-3xl font-black text-slate-900 tracking-tighter uppercase mb-2">
|
||||
{editingEvent ? 'Edit Portal' : 'Create Portal'}
|
||||
</h2>
|
||||
<p className="text-[#004a99] font-bold text-[8px] tracking-[0.4em] uppercase mb-10">
|
||||
External Link Broadcaster
|
||||
</p>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div>
|
||||
<label className="block text-[9px] font-black text-slate-500 uppercase tracking-[0.2em] mb-3">Event Title</label>
|
||||
<input
|
||||
required
|
||||
type="text"
|
||||
placeholder="e.g. Google Cloud Workshop"
|
||||
className="w-full bg-slate-50 border border-slate-200 rounded-xl px-6 py-4 text-slate-900 focus:ring-1 focus:ring-[#004a99] outline-none font-bold placeholder:text-slate-400 transition-all"
|
||||
value={formData.title}
|
||||
onChange={e => setFormData({ ...formData, title: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-[9px] font-black text-slate-500 uppercase tracking-[0.2em] mb-3">External Link</label>
|
||||
<input
|
||||
required
|
||||
type="url"
|
||||
placeholder="https://..."
|
||||
className="w-full bg-slate-50 border border-slate-200 rounded-xl px-6 py-4 text-slate-900 focus:ring-1 focus:ring-[#004a99] outline-none font-bold placeholder:text-slate-400 transition-all"
|
||||
value={formData.link}
|
||||
onChange={e => setFormData({ ...formData, link: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-[9px] font-black text-slate-500 uppercase tracking-[0.2em] mb-3">Description</label>
|
||||
<textarea
|
||||
required
|
||||
placeholder="What is this event about?"
|
||||
className="w-full bg-slate-50 border border-slate-200 rounded-2xl px-6 py-4 text-slate-900 focus:ring-1 focus:ring-[#004a99] outline-none font-bold placeholder:text-slate-400 transition-all min-h-[120px] resize-none"
|
||||
value={formData.description}
|
||||
onChange={e => setFormData({ ...formData, description: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 pt-4">
|
||||
{editingEvent && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setEditingEvent(null);
|
||||
setFormData({ title: '', description: '', link: '' });
|
||||
}}
|
||||
className="flex-1 py-4 bg-slate-100 text-slate-600 font-black uppercase tracking-widest text-[10px] rounded-2xl hover:bg-slate-200 transition-all"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="flex-[2] py-4 bg-[#004a99] text-white font-black uppercase tracking-widest text-[10px] rounded-2xl hover:bg-blue-800 transition-all shadow-lg active:scale-95 disabled:opacity-50"
|
||||
>
|
||||
{isSubmitting ? 'Syncing...' : editingEvent ? 'Update Link' : 'Generate Link'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* List of Special Events */}
|
||||
<div className="lg:col-span-2">
|
||||
<div className="flex items-end justify-between mb-12 border-b border-slate-200 pb-8">
|
||||
<div>
|
||||
<h2 className="text-5xl font-black text-slate-900 tracking-tighter uppercase mb-2">Active Portals</h2>
|
||||
<p className="text-[#004a99] font-bold text-xs tracking-[0.4em] uppercase flex items-center gap-3">
|
||||
<span className="w-1.5 h-1.5 bg-[#004a99] rounded-full"></span>
|
||||
External Event Hub
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<span className="text-[32px] font-black text-slate-900">{specialEvents.length}</span>
|
||||
<p className="text-[9px] font-black text-slate-400 uppercase tracking-widest">Global Links</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
{isLoading ? (
|
||||
<div className="py-20 flex flex-col items-center justify-center">
|
||||
<div className="w-10 h-10 border-4 border-slate-200 border-t-[#004a99] rounded-full animate-spin"></div>
|
||||
</div>
|
||||
) : specialEvents.length > 0 ? (
|
||||
specialEvents.map((event) => (
|
||||
<div key={event.id} className="bg-white border border-slate-100 rounded-[2.5rem] p-10 shadow-sm hover:shadow-2xl hover:shadow-slate-200 transition-all group flex flex-col md:flex-row md:items-center gap-10 border-l-8 border-l-[#004a99] relative overflow-hidden">
|
||||
<div className="absolute -top-10 -right-10 w-40 h-40 bg-[#004a99]/5 rounded-full blur-3xl group-hover:bg-[#004a99]/10 transition-colors duration-1000"></div>
|
||||
|
||||
<div className="flex-1 relative z-10">
|
||||
<div className="flex flex-wrap items-center gap-4 mb-6">
|
||||
<div className="flex items-center gap-3">
|
||||
{event.verification_status === 'APPROVED' ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-2 h-2 rounded-full bg-emerald-500 shadow-[0_0_8px_rgba(16,185,129,0.5)]"></span>
|
||||
<span className="text-[9px] font-black text-slate-400 uppercase tracking-widest leading-none">Broadcast Active</span>
|
||||
</div>
|
||||
) : event.verification_status === 'REJECTED' ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-2 h-2 rounded-full bg-rose-500"></span>
|
||||
<span className="text-[9px] font-black text-rose-400 uppercase tracking-widest leading-none">Rejected</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-2 h-2 rounded-full bg-amber-500 animate-pulse"></span>
|
||||
<span className="text-[9px] font-black text-amber-500 uppercase tracking-widest leading-none">Security Review Queue</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<span className="w-1 h-1 bg-slate-200 rounded-full"></span>
|
||||
<span className="text-[8px] font-black text-slate-300 uppercase tracking-[0.3em] font-inter">Global ID: {event.id.slice(0, 8)}</span>
|
||||
</div>
|
||||
|
||||
<h3 className="text-3xl font-black text-slate-900 mb-3 group-hover:text-[#004a99] transition-colors tracking-tight uppercase leading-none">{event.title}</h3>
|
||||
<p className="text-blue-500 font-bold text-[10px] mb-4 truncate max-w-md">{event.link}</p>
|
||||
<p className="text-slate-500 text-sm font-medium line-clamp-2 italic">{event.description}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={() => handleEdit(event)}
|
||||
className="w-12 h-12 bg-blue-50 text-[#004a99] rounded-2xl flex items-center justify-center hover:bg-[#004a99] hover:text-white transition-all active:scale-90 shadow-sm"
|
||||
title="Edit Portal"
|
||||
>
|
||||
<i className="fas fa-edit text-sm"></i>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(event.id)}
|
||||
className="w-12 h-12 bg-rose-50 text-rose-500 rounded-2xl flex items-center justify-center hover:bg-rose-500 hover:text-white transition-all active:scale-90 shadow-sm"
|
||||
title="Delete Portal"
|
||||
>
|
||||
<i className="fas fa-trash-alt text-sm"></i>
|
||||
</button>
|
||||
<a
|
||||
href={event.link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="w-12 h-12 bg-slate-900 text-white rounded-2xl flex items-center justify-center hover:bg-black transition-all active:scale-90 shadow-lg shadow-slate-900/20"
|
||||
title="Visit Link"
|
||||
>
|
||||
<i className="fas fa-external-link-alt text-sm"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="py-20 text-center bg-slate-50 border border-dashed border-slate-300 rounded-[2rem]">
|
||||
<i className="fas fa-link text-4xl text-slate-300 mb-6"></i>
|
||||
<p className="text-slate-400 font-black uppercase tracking-widest text-xs">No special events found</p>
|
||||
<p className="text-slate-300 font-bold text-[9px] uppercase tracking-widest mt-2">Start by creating your first global portal</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateSpecialEventsView;
|
||||
335
RIT-EVENT-MANAGEMENT--main/components/Dashboard.tsx
Normal file
335
RIT-EVENT-MANAGEMENT--main/components/Dashboard.tsx
Normal file
@@ -0,0 +1,335 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { UserRole, DashboardView, Event, Announcement, Ticket } from '../types';
|
||||
import { supabase } from '../supabase';
|
||||
import Navbar from './Navbar';
|
||||
import Hero from './Hero';
|
||||
import HomeDashboard from './HomeDashboard';
|
||||
import EventList from './EventList';
|
||||
import EventCard from './EventCard';
|
||||
import ProfileView from './ProfileView';
|
||||
import StatusTrackerView from './StatusTrackerView';
|
||||
import RegistrationsView from './RegistrationsView';
|
||||
import UpcomingEventsSlider from './UpcomingEventsSlider';
|
||||
import AboutHubSection from './AboutHubSection';
|
||||
import StatsSection from './StatsSection';
|
||||
import AboutSection from './AboutSection';
|
||||
import ContactSection from './ContactSection';
|
||||
import Footer from './Footer';
|
||||
|
||||
const Portal: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
return createPortal(children, document.body);
|
||||
};
|
||||
|
||||
interface DashboardProps {
|
||||
userRole: UserRole;
|
||||
events: Event[];
|
||||
announcements: Announcement[];
|
||||
bookedEventIds: string[];
|
||||
userRegistrations: any[];
|
||||
onToggleBooking: (id: string) => void;
|
||||
onLogout: () => void;
|
||||
onUploadCertificate: (eventId: string, data: string) => Promise<string>;
|
||||
studentDocuments: Record<string, string>;
|
||||
onUploadDoc: (studentId: string, data: string) => void;
|
||||
currentUserName: string;
|
||||
onSupabaseError?: () => void;
|
||||
onBackToCoordinatorHub?: () => void;
|
||||
specialEvents: any[];
|
||||
}
|
||||
|
||||
const Dashboard: React.FC<DashboardProps> = ({
|
||||
userRole,
|
||||
events,
|
||||
announcements,
|
||||
bookedEventIds,
|
||||
userRegistrations,
|
||||
onToggleBooking,
|
||||
onLogout,
|
||||
onUploadCertificate,
|
||||
studentDocuments,
|
||||
onUploadDoc,
|
||||
currentUserName,
|
||||
onSupabaseError,
|
||||
onBackToCoordinatorHub,
|
||||
specialEvents
|
||||
}) => {
|
||||
const [activeView, setActiveView] = useState<DashboardView>('HOME');
|
||||
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
|
||||
const [trackedEvent, setTrackedEvent] = useState<Event | null>(null);
|
||||
|
||||
// Team Management States
|
||||
const [showCreateModal, setShowCreateModal] = useState<{eventId: string, title: string} | null>(null);
|
||||
const [showJoinModal, setShowJoinModal] = useState<{eventId: string, title: string} | null>(null);
|
||||
const [teamName, setTeamName] = useState('');
|
||||
const [teamCode, setTeamCode] = useState('');
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
|
||||
const generateCode = () => Math.random().toString(36).substring(2, 8).toUpperCase();
|
||||
|
||||
const handleCreateTeam = async (eventId: string) => {
|
||||
if (!teamName.trim()) return alert("Enter team name");
|
||||
setIsProcessing(true);
|
||||
try {
|
||||
const code = generateCode();
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
if (!user) return;
|
||||
|
||||
const registrationId = `${user.id}_${eventId}`;
|
||||
const { error } = await supabase.from('registrations').update({
|
||||
team_code: code,
|
||||
team_name: teamName.trim(),
|
||||
is_team_leader: true
|
||||
}).eq('id', registrationId);
|
||||
|
||||
if (error) throw error;
|
||||
setShowCreateModal(null);
|
||||
setTeamName('');
|
||||
window.location.reload();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert("Failed to create team");
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleJoinTeam = async (eventId: string) => {
|
||||
if (!teamCode.trim()) return alert("Enter team code");
|
||||
setIsProcessing(true);
|
||||
try {
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
if (!user) return;
|
||||
|
||||
const { data: teamRegs, error: fetchErr } = await supabase
|
||||
.from('registrations')
|
||||
.select('*')
|
||||
.eq('event_id', eventId)
|
||||
.eq('team_code', teamCode.trim().toUpperCase());
|
||||
|
||||
if (fetchErr) throw fetchErr;
|
||||
if (!teamRegs || teamRegs.length === 0) return alert("Invalid team code for this event");
|
||||
|
||||
const leader = teamRegs.find(r => r.is_team_leader);
|
||||
const event = events.find(e => e.id === eventId);
|
||||
|
||||
if (event?.teamSizeLimit && teamRegs.length >= event.teamSizeLimit) {
|
||||
return alert("Team is already full");
|
||||
}
|
||||
|
||||
const { data: myProfile } = await supabase.from('Studentusers').select('department').eq('id', user.id).single();
|
||||
if (event?.teamComposition === 'INTER_DEPT' && leader && myProfile?.department !== leader.dept) {
|
||||
return alert(`This event requires INTER-DEPARTMENT teams. You must join a team from ${leader.dept}`);
|
||||
}
|
||||
|
||||
const registrationId = `${user.id}_${eventId}`;
|
||||
const { error: joinErr } = await supabase.from('registrations').update({
|
||||
team_code: teamCode.trim().toUpperCase(),
|
||||
team_name: leader?.team_name || 'Team',
|
||||
is_team_leader: false
|
||||
}).eq('id', registrationId);
|
||||
|
||||
if (joinErr) throw joinErr;
|
||||
setShowJoinModal(null);
|
||||
setTeamCode('');
|
||||
window.location.reload();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert("Failed to join team");
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (activeView !== 'EVENTS' && activeView !== 'STATUS_TRACKER') {
|
||||
setSelectedCategory(null);
|
||||
}
|
||||
}, [activeView]);
|
||||
|
||||
const handleTrackStatus = (event: Event) => {
|
||||
setTrackedEvent(event);
|
||||
setActiveView('STATUS_TRACKER');
|
||||
};
|
||||
|
||||
const renderContent = () => {
|
||||
switch (activeView) {
|
||||
case 'HOME':
|
||||
return (
|
||||
<>
|
||||
<Hero events={events} />
|
||||
<UpcomingEventsSlider events={events} />
|
||||
<AboutHubSection />
|
||||
<StatsSection events={events} />
|
||||
<HomeDashboard
|
||||
events={events}
|
||||
announcements={announcements}
|
||||
onNavigateToEvents={() => setActiveView('EVENTS')}
|
||||
specialEvents={specialEvents}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
case 'ABOUT':
|
||||
return (
|
||||
<div className="pt-16 min-h-screen bg-white">
|
||||
<AboutSection />
|
||||
</div>
|
||||
);
|
||||
case 'CONTACT':
|
||||
return (
|
||||
<div className="pt-16 min-h-screen bg-white">
|
||||
<ContactSection />
|
||||
</div>
|
||||
);
|
||||
case 'EVENTS':
|
||||
return (
|
||||
<EventList
|
||||
events={events}
|
||||
selectedCategory={selectedCategory}
|
||||
bookedEventIds={bookedEventIds}
|
||||
onToggleBooking={onToggleBooking}
|
||||
onSelectCategory={setSelectedCategory}
|
||||
onTrackStatus={handleTrackStatus}
|
||||
currentUserName={currentUserName}
|
||||
userRole={userRole}
|
||||
userRegistrations={userRegistrations}
|
||||
/>
|
||||
);
|
||||
case 'REGISTRATIONS':
|
||||
return (
|
||||
<RegistrationsView
|
||||
events={events}
|
||||
bookedEventIds={bookedEventIds}
|
||||
userRegistrations={userRegistrations}
|
||||
onToggleBooking={onToggleBooking}
|
||||
onTrackStatus={handleTrackStatus}
|
||||
currentUserName={currentUserName}
|
||||
userRole={userRole}
|
||||
/>
|
||||
);
|
||||
case 'PROFILE':
|
||||
return <ProfileView onLogout={onLogout} onSupabaseError={onSupabaseError} />;
|
||||
case 'STATUS_TRACKER':
|
||||
const registration = userRegistrations.find(r => String(r.event_id) === String(trackedEvent?.id));
|
||||
return trackedEvent ? (
|
||||
<StatusTrackerView
|
||||
event={trackedEvent}
|
||||
registration={registration}
|
||||
onBack={() => setActiveView('REGISTRATIONS')}
|
||||
onUploadCertificate={(data) => onUploadCertificate(trackedEvent.id, data)}
|
||||
onShowCreateTeam={() => setShowCreateModal({ eventId: trackedEvent.id, title: trackedEvent.title })}
|
||||
onShowJoinTeam={() => setShowJoinModal({ eventId: trackedEvent.id, title: trackedEvent.title })}
|
||||
/>
|
||||
) : null;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#F3F4F6]">
|
||||
<Navbar
|
||||
activeView={activeView}
|
||||
onViewChange={setActiveView}
|
||||
onLogout={onLogout}
|
||||
onBackToCoordinatorHub={onBackToCoordinatorHub}
|
||||
/>
|
||||
<main className="pb-20">
|
||||
{renderContent()}
|
||||
</main>
|
||||
<Footer />
|
||||
|
||||
{/* Team Management Modals */}
|
||||
{showCreateModal && (
|
||||
<Portal>
|
||||
<div className="fixed inset-0 z-[10000] flex items-center justify-center p-6 bg-slate-900/60 backdrop-blur-sm animate-in fade-in duration-300">
|
||||
<div className="bg-white rounded-[3rem] w-full max-w-md p-10 md:p-12 shadow-2xl animate-in zoom-in-95 duration-500 relative overflow-hidden">
|
||||
<div className="absolute top-0 left-0 w-full h-2 bg-gradient-to-r from-orange-400 to-amber-400"></div>
|
||||
<div className="w-16 h-16 flex items-center justify-center mx-auto mb-8">
|
||||
<img
|
||||
src="https://www.facultyplus.com/wp-content/uploads/2020/11/Rit-logo.png"
|
||||
alt="RIT Logo"
|
||||
className="w-full h-full object-contain"
|
||||
/>
|
||||
</div>
|
||||
<h3 className="text-2xl font-black text-slate-900 text-center uppercase mb-2 tracking-tight">Form a Team</h3>
|
||||
<p className="text-[10px] text-gray-400 text-center font-bold uppercase tracking-widest mb-10 line-clamp-1 px-4">{showCreateModal.title}</p>
|
||||
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
<label className="block text-[9px] font-black text-slate-400 uppercase tracking-[0.2em] mb-3 px-2">Team Identity</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Enter team name..."
|
||||
className="w-full bg-slate-50 border border-slate-200 rounded-[2rem] px-8 py-5 text-sm font-bold outline-none focus:ring-4 focus:ring-orange-100 focus:border-[#f97316] transition-all placeholder:text-slate-300"
|
||||
value={teamName}
|
||||
onChange={e => setTeamName(e.target.value)}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
<button
|
||||
onClick={() => handleCreateTeam(showCreateModal.eventId)}
|
||||
disabled={isProcessing}
|
||||
className="w-full py-5 bg-[#f97316] text-white rounded-[2rem] font-black uppercase text-[11px] tracking-[0.2em] shadow-xl shadow-orange-200 hover:bg-[#ea580c] transition-all active:scale-95 disabled:opacity-50"
|
||||
>
|
||||
{isProcessing ? 'INITIALIZING...' : 'Establish Team & Code'}
|
||||
</button>
|
||||
<button onClick={() => setShowCreateModal(null)} className="w-full py-4 text-gray-400 font-black uppercase text-[9px] tracking-widest hover:text-slate-600 transition-colors">Dismiss</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Portal>
|
||||
)}
|
||||
|
||||
{showJoinModal && (
|
||||
<Portal>
|
||||
<div className="fixed inset-0 z-[10000] flex items-center justify-center p-6 bg-slate-900/60 backdrop-blur-sm animate-in fade-in duration-300">
|
||||
<div className="bg-white rounded-[3rem] w-full max-w-md p-10 md:p-12 shadow-2xl animate-in zoom-in-95 duration-500 relative overflow-hidden">
|
||||
<div className="absolute top-0 left-0 w-full h-2 bg-gradient-to-r from-blue-400 to-indigo-400"></div>
|
||||
<div className="w-16 h-16 flex items-center justify-center mx-auto mb-8">
|
||||
<img
|
||||
src="https://www.facultyplus.com/wp-content/uploads/2020/11/Rit-logo.png"
|
||||
alt="RIT Logo"
|
||||
className="w-full h-full object-contain"
|
||||
/>
|
||||
</div>
|
||||
<h3 className="text-2xl font-black text-slate-900 text-center uppercase mb-2 tracking-tight">Join Alliance</h3>
|
||||
<p className="text-[10px] text-gray-400 text-center font-bold uppercase tracking-widest mb-10 line-clamp-1 px-4">{showJoinModal.title}</p>
|
||||
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
<label className="block text-[9px] font-black text-slate-400 uppercase tracking-[0.2em] mb-3 px-2 text-center">Team Access Token</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="TOKEN"
|
||||
className="w-full bg-slate-50 border border-slate-200 rounded-[2rem] px-5 py-6 text-center text-3xl font-black tracking-[0.4em] outline-none focus:ring-4 focus:ring-blue-100 focus:border-blue-500 uppercase transition-all placeholder:text-slate-200"
|
||||
value={teamCode}
|
||||
onChange={e => setTeamCode(e.target.value)}
|
||||
maxLength={6}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
<button
|
||||
onClick={() => handleJoinTeam(showJoinModal.eventId)}
|
||||
disabled={isProcessing}
|
||||
className="w-full py-5 bg-slate-900 text-white rounded-[2rem] font-black uppercase text-[11px] tracking-[0.2em] shadow-xl shadow-gray-200 hover:bg-black transition-all active:scale-95 disabled:opacity-50"
|
||||
>
|
||||
{isProcessing ? 'AUTHENTICATING...' : 'Validate & Join'}
|
||||
</button>
|
||||
<button onClick={() => setShowJoinModal(null)} className="w-full py-4 text-gray-400 font-black uppercase text-[9px] tracking-widest hover:text-slate-600 transition-colors">Dismiss</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Portal>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
70
RIT-EVENT-MANAGEMENT--main/components/DomainSelection.tsx
Normal file
70
RIT-EVENT-MANAGEMENT--main/components/DomainSelection.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { supabase } from '../supabase';
|
||||
|
||||
interface DomainSelectionProps {
|
||||
category: string;
|
||||
onSelectDomain: (domainId: string, domainName?: string) => void;
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
const DomainSelection: React.FC<DomainSelectionProps> = ({ category, onSelectDomain, onBack }) => {
|
||||
const [domains, setDomains] = useState<any[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchDomains = async () => {
|
||||
const { data } = await supabase.from('domains').select('*').eq('status', 'APPROVED').eq('category', category);
|
||||
if (data) {
|
||||
setDomains(data.sort((a: any, b: any) => a.name.localeCompare(b.name)));
|
||||
}
|
||||
};
|
||||
fetchDomains();
|
||||
}, [category]);
|
||||
|
||||
return (
|
||||
<section className="pt-40 pb-12 px-6 md:px-12 lg:px-24 bg-white animate-in fade-in slide-in-from-right-4 duration-500">
|
||||
<button
|
||||
onClick={onBack}
|
||||
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>
|
||||
BACK TO CATEGORIES
|
||||
</button>
|
||||
|
||||
<h2 className="text-3xl md:text-4xl font-black text-center text-[#1A202C] mb-12 tracking-tighter uppercase">
|
||||
CHOOSE YOUR <span className="text-[#f97316]">DOMAIN</span>
|
||||
</h2>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-3 gap-8">
|
||||
{domains.map((domain) => (
|
||||
<div
|
||||
key={domain.id}
|
||||
onClick={() => onSelectDomain(domain.name, domain.name)}
|
||||
className="group relative h-[350px] rounded-[2rem] overflow-hidden cursor-pointer shadow-[0_10px_30px_rgba(0,0,0,0.1)] transition-all duration-500 hover:-translate-y-2 hover:shadow-2xl"
|
||||
>
|
||||
<img
|
||||
src={domain.image}
|
||||
alt={domain.name}
|
||||
className="w-full h-full object-cover transition-transform duration-1000 group-hover:scale-110"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/20 to-transparent opacity-90"></div>
|
||||
|
||||
<div className="absolute bottom-8 left-8 right-8 flex flex-col items-start">
|
||||
<span className="bg-[#f97316] text-[10px] font-black text-white px-3 py-1 rounded-full uppercase tracking-[0.2em] mb-3">
|
||||
Domain
|
||||
</span>
|
||||
<h3 className="text-2xl font-black text-white tracking-tight transform transition-transform duration-500 group-hover:translate-x-2">
|
||||
{domain.name}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="absolute top-6 right-6 w-10 h-10 bg-white rounded-full flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity border border-gray-200">
|
||||
<i className="fas fa-chevron-right text-gray-800 text-xs"></i>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default DomainSelection;
|
||||
559
RIT-EVENT-MANAGEMENT--main/components/EventCard.tsx
Normal file
559
RIT-EVENT-MANAGEMENT--main/components/EventCard.tsx
Normal file
@@ -0,0 +1,559 @@
|
||||
import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { toPng } from 'html-to-image';
|
||||
import { Event } from '../types';
|
||||
import { supabase } from '../supabase';
|
||||
import { CLUBS } from '../constants';
|
||||
|
||||
interface EventCardProps {
|
||||
event: Event;
|
||||
isBooked: boolean;
|
||||
onToggle: () => void;
|
||||
onTrackStatus?: (event: Event) => void;
|
||||
currentUserName?: string;
|
||||
userRole?: string;
|
||||
registration?: any;
|
||||
}
|
||||
|
||||
const Portal: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
return createPortal(children, document.body);
|
||||
};
|
||||
|
||||
const EventCard: React.FC<EventCardProps> = ({ event, isBooked, onToggle, onTrackStatus, currentUserName = 'Student', userRole, registration }) => {
|
||||
const [showConfirm, setShowConfirm] = useState(false);
|
||||
const clubInfo = useMemo(() => CLUBS.find(c => c.name === event.club), [event.club]);
|
||||
const [showCancelConfirm, setShowCancelConfirm] = useState(false);
|
||||
const [showTicket, setShowTicket] = useState(false);
|
||||
const [showSummary, setShowSummary] = useState(false);
|
||||
const [userDept, setUserDept] = useState<string>('');
|
||||
const [userYear, setUserYear] = useState<string>('');
|
||||
const [userSection, setUserSection] = useState<string>('');
|
||||
const [now, setNow] = useState(new Date());
|
||||
const [isDownloading, setIsDownloading] = useState(false);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const ticketRef = useRef<HTMLDivElement>(null);
|
||||
const [persistedTicket, setPersistedTicket] = useState<{id: string, qr: string} | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (showTicket && registration?.id) {
|
||||
const initializeTicket = async () => {
|
||||
// 1. Check if already persisted
|
||||
if (registration.ticket_id && registration.ticket_qrcode) {
|
||||
setPersistedTicket({ id: registration.ticket_id, qr: registration.ticket_qrcode });
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Otherwise generate and save
|
||||
try {
|
||||
const newId = `RIT-EVT-${Math.random().toString(36).substring(2, 6).toUpperCase()}-${registration.id.substring(0, 4).toUpperCase()}`;
|
||||
const newQr = `${window.location.origin}/?verify=${registration.id}`;
|
||||
|
||||
const { error } = await supabase
|
||||
.from('registrations')
|
||||
.update({ ticket_id: newId, ticket_qrcode: newQr })
|
||||
.eq('id', registration.id);
|
||||
|
||||
if (!error) {
|
||||
setPersistedTicket({ id: newId, qr: newQr });
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Ticket initialization failed:", err);
|
||||
}
|
||||
};
|
||||
initializeTicket();
|
||||
}
|
||||
}, [showTicket, registration?.id]);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => setNow(new Date()), 1000);
|
||||
supabase.auth.getUser().then(({ data: { user } }) => {
|
||||
if (user) {
|
||||
supabase.from('Studentusers').select('department, year, section').eq('id', user.id).single()
|
||||
.then(({ data }) => {
|
||||
if (data?.department) {
|
||||
setUserDept(data.department);
|
||||
setUserYear(data.year || '');
|
||||
setUserSection(data.section || '');
|
||||
} else {
|
||||
supabase.from('externalusers').select('department, year, section').eq('id', user.id).single()
|
||||
.then(({ data: extData }) => {
|
||||
if (extData?.department) {
|
||||
setUserDept(extData.department);
|
||||
setUserYear(extData.year || '');
|
||||
setUserSection(extData.section || '');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
return () => clearInterval(timer);
|
||||
}, []);
|
||||
|
||||
const admissionStatus = useMemo(() => {
|
||||
if (event.status === 'Completed') return 'EVENT_ENDED';
|
||||
|
||||
if (event.registrationDeadline) {
|
||||
const deadline = new Date(event.registrationDeadline);
|
||||
if (now > deadline) return 'DEADLINE_PASSED';
|
||||
}
|
||||
|
||||
if (userDept && event.deptLimits?.[userDept]) {
|
||||
const sectionLimits = event.deptSectionLimits?.[userDept];
|
||||
const hasSectionLimits = sectionLimits && Object.keys(sectionLimits).length > 0;
|
||||
|
||||
if (hasSectionLimits) {
|
||||
const limit = sectionLimits[userSection];
|
||||
if (!limit || limit <= 0) {
|
||||
return 'SECTION_NOT_ALLOWED';
|
||||
}
|
||||
const currentSectionCount = event.currentDeptSectionCounts?.[userDept]?.[userSection] || 0;
|
||||
if (currentSectionCount >= limit) {
|
||||
return 'SECTION_FULL';
|
||||
}
|
||||
} else {
|
||||
const currentDeptCount = event.currentDeptCounts?.[userDept] || 0;
|
||||
if (currentDeptCount >= event.deptLimits[userDept]) return 'DEPT_FULL';
|
||||
}
|
||||
}
|
||||
|
||||
if (event.maxParticipants && (event.currentParticipants || 0) >= event.maxParticipants) {
|
||||
return 'TOTAL_FULL';
|
||||
}
|
||||
|
||||
return 'OPEN';
|
||||
}, [event, now, userDept, userSection]);
|
||||
|
||||
const timeLeft = useMemo(() => {
|
||||
const targetDate = new Date(event.date).getTime();
|
||||
const distance = targetDate - now.getTime();
|
||||
if (isNaN(targetDate) || distance <= 0) return { d: '00', h: '00', m: '00', s: '00' };
|
||||
return {
|
||||
d: Math.floor(distance / (1000 * 60 * 60 * 24)).toString().padStart(2, '0'),
|
||||
h: Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)).toString().padStart(2, '0'),
|
||||
m: Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60)).toString().padStart(2, '0'),
|
||||
s: Math.floor((distance % (1000 * 60)) / 1000).toString().padStart(2, '0')
|
||||
};
|
||||
}, [event.date, now]);
|
||||
|
||||
const handleDownload = async () => {
|
||||
if (!ticketRef.current) return;
|
||||
setIsDownloading(true);
|
||||
try {
|
||||
const dataUrl = await toPng(ticketRef.current, { cacheBust: true, quality: 1, backgroundColor: '#1e293b' });
|
||||
const link = document.createElement('a');
|
||||
link.download = `Pass_${event.title.replace(/\s+/g, '_')}.png`;
|
||||
link.href = dataUrl;
|
||||
link.click();
|
||||
} catch (err) {
|
||||
console.error("Capture failed:", err);
|
||||
} finally {
|
||||
setIsDownloading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const verificationLink = `${window.location.origin}/?verify=local_${event.id}`;
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-[40px] overflow-hidden shadow-xl shadow-gray-900/5 transition-all duration-500 hover:-translate-y-2 flex flex-col h-full group border border-gray-200">
|
||||
<div className="h-64 relative overflow-hidden">
|
||||
<img src={event.image} alt={event.title} className="w-full h-full object-cover transition-transform duration-1000 group-hover:scale-110" />
|
||||
<div className="absolute top-6 left-6 flex flex-col gap-3">
|
||||
<div className="bg-white px-5 py-2 rounded-2xl text-[10px] font-black uppercase tracking-widest text-[#f97316] shadow-xl border border-gray-200">{event.category}</div>
|
||||
{event.registrationDeadline && (
|
||||
<div className={`px-5 py-2 rounded-2xl text-[9px] font-black uppercase tracking-widest text-white shadow-xl flex items-center gap-2 ${admissionStatus === 'DEADLINE_PASSED' ? 'bg-[#1A202C]' : 'bg-rose-600 animate-pulse'}`}>
|
||||
<i className="fas fa-clock"></i>
|
||||
{admissionStatus === 'DEADLINE_PASSED' ? 'Closed' : `Ends ${new Date(event.registrationDeadline).toLocaleDateString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })}`}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-10 flex flex-col flex-1">
|
||||
<div className="flex justify-between items-start mb-6">
|
||||
<div className="flex-1">
|
||||
{clubInfo && (
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<img
|
||||
src={clubInfo.image}
|
||||
alt={clubInfo.name}
|
||||
className="w-12 h-12 rounded-full object-cover border border-gray-100 shadow-sm"
|
||||
referrerPolicy="no-referrer"
|
||||
/>
|
||||
<span className="text-[10px] font-black text-[#f97316] uppercase tracking-widest">{clubInfo.name}</span>
|
||||
</div>
|
||||
)}
|
||||
<h3 className="text-3xl font-black text-[#1A202C] line-clamp-2 uppercase tracking-tight group-hover:text-[#f97316] transition-colors">{event.title}</h3>
|
||||
</div>
|
||||
<div className={`px-4 py-1.5 rounded-full text-[9px] font-black uppercase tracking-widest border ${event.pricingType === 'PAID' ? 'bg-rose-50 border-rose-100 text-rose-600' : 'bg-emerald-50 border-emerald-100 text-emerald-600'}`}>{event.pricingType}</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 mb-8 text-gray-400 font-bold text-xs uppercase tracking-widest">
|
||||
<div className="flex items-center gap-4">
|
||||
<i className="far fa-calendar-alt w-4 text-[#f97316]"></i>
|
||||
<span>{event.date}{event.schedule?.[0]?.start_time ? ` • ${event.schedule[0].start_time}` : ''}</span>
|
||||
</div>
|
||||
{event.durationDays && event.durationDays > 1 && (
|
||||
<div className="flex items-center gap-4">
|
||||
<i className="fas fa-hourglass-half w-4 text-[#f97316]"></i>
|
||||
<span>Duration: {event.durationDays} Days</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<div className="flex items-center gap-4">
|
||||
<i className="fas fa-location-dot w-4 text-[#f97316]"></i>
|
||||
<span>{event.location}</span>
|
||||
</div>
|
||||
{event.event_summary && (
|
||||
<button
|
||||
onClick={() => setShowSummary(true)}
|
||||
className="w-7 h-7 flex items-center justify-center rounded-xl bg-slate-50 border border-slate-200 text-slate-400 hover:bg-[#f97316] hover:text-white hover:border-[#f97316] transition-all shadow-sm group"
|
||||
title="View summary"
|
||||
>
|
||||
<i className="fas fa-info text-[10px] group-hover:scale-110 transition-transform"></i>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{event.schedule && event.schedule.length > 0 && (() => {
|
||||
// Group schedule entries by day_idx
|
||||
const dayMap = new Map<number, typeof event.schedule>();
|
||||
event.schedule!.forEach(s => {
|
||||
if (!dayMap.has(s.day_idx)) dayMap.set(s.day_idx, []);
|
||||
dayMap.get(s.day_idx)!.push(s);
|
||||
});
|
||||
const sortedDays = Array.from(dayMap.entries()).sort((a, b) => a[0] - b[0]);
|
||||
|
||||
return (
|
||||
<div className="mb-8 space-y-4">
|
||||
<span className="text-[9px] font-black text-gray-400 uppercase tracking-widest block mb-2">Event Schedule</span>
|
||||
<div className="space-y-4">
|
||||
{sortedDays.map(([dayIdx, slots]) => (
|
||||
<div key={dayIdx} className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[10px] font-black text-[#1A202C] uppercase tracking-widest">Day {dayIdx}</span>
|
||||
<span className="text-[9px] font-bold text-gray-400 uppercase">{slots![0]?.date}</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{slots!.map((slot) => (
|
||||
<div key={slot.id} className="bg-gray-50 border border-gray-100 rounded-xl px-3 py-2 flex flex-col">
|
||||
<span className="text-[8px] font-black text-[#f97316] uppercase tracking-tighter">Batch {slot.batch_idx}</span>
|
||||
<span className="text-[10px] font-bold text-gray-600">{slot.start_time} - {slot.end_time}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
|
||||
<div className="mb-8 p-6 bg-gray-50 rounded-3xl border border-gray-200">
|
||||
<div className="flex justify-between items-center mb-3">
|
||||
<span className="text-[10px] font-black text-gray-400 uppercase tracking-widest">Global Seats</span>
|
||||
<span className="text-xs font-black text-[#1A202C]">{event.currentParticipants || 0} / {event.maxParticipants || (event.deptLimits && Object.keys(event.deptLimits).length > 0 ? Object.values(event.deptLimits).map(Number).reduce((a, b) => a + b, 0) : '∞')}</span>
|
||||
</div>
|
||||
{userDept && event.deptLimits?.[userDept] && (() => {
|
||||
const sectionLimits = event.deptSectionLimits?.[userDept];
|
||||
const hasSectionLimits = sectionLimits && Object.keys(sectionLimits).length > 0;
|
||||
if (hasSectionLimits) {
|
||||
const limit = sectionLimits[userSection] || 0;
|
||||
const current = event.currentDeptSectionCounts?.[userDept]?.[userSection] || 0;
|
||||
return (
|
||||
<div className="flex justify-between items-center pt-3 border-t border-gray-200">
|
||||
<span className="text-[10px] font-black text-[#f97316] uppercase tracking-widest">{userDept} Sec {userSection || 'N/A'} Allocation</span>
|
||||
<span className={`text-xs font-black ${(admissionStatus === 'SECTION_FULL' || admissionStatus === 'SECTION_NOT_ALLOWED') ? 'text-rose-500' : 'text-[#1A202C]'}`}>
|
||||
{limit > 0 ? `${current} / ${limit}` : 'RESTRICTED'}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="flex justify-between items-center pt-3 border-t border-gray-200">
|
||||
<span className="text-[10px] font-black text-[#f97316] uppercase tracking-widest">{userDept} Allocation</span>
|
||||
<span className={`text-xs font-black ${admissionStatus === 'DEPT_FULL' ? 'text-rose-500' : 'text-[#1A202C]'}`}>
|
||||
{event.currentDeptCounts?.[userDept] || 0} / {event.deptLimits[userDept]}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center mb-10 text-center">
|
||||
{[ {l: 'D', v: timeLeft.d}, {l: 'H', v: timeLeft.h}, {l: 'M', v: timeLeft.m}, {l: 'S', v: timeLeft.s} ].map((t, i) => (
|
||||
<div key={i} className="flex-1">
|
||||
<span className={`block text-2xl font-black leading-none ${i === 3 ? 'text-[#f97316]' : 'text-[#1A202C]'}`}>{t.v}</span>
|
||||
<span className="text-[9px] font-black text-gray-400 uppercase tracking-widest">{t.l}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-auto">
|
||||
{userRole === 'ADMIN' || userRole === 'COORDINATOR' ? (
|
||||
<button disabled className="w-full py-5 rounded-[2rem] bg-slate-100 border border-slate-200 text-slate-400 font-black uppercase tracking-[0.3em] text-xs cursor-not-allowed">
|
||||
View Only ({userRole === 'ADMIN' ? 'Admin' : 'Coordinator'})
|
||||
</button>
|
||||
) : isBooked ? (
|
||||
<div className="flex flex-col gap-4">
|
||||
<button onClick={() => onTrackStatus?.(event)} className="w-full py-5 rounded-[2rem] bg-[#f97316] text-white font-black uppercase tracking-[0.3em] text-xs shadow-xl shadow-orange-100 flex items-center justify-center gap-3 active:scale-[0.98] transition-all">
|
||||
Track Progress <i className="fas fa-arrow-right"></i>
|
||||
</button>
|
||||
{event.status !== 'Completed' && (
|
||||
<button onClick={() => setShowTicket(true)} className="w-full py-5 rounded-[2rem] bg-[#1A202C] text-white font-black uppercase tracking-[0.3em] text-xs hover:bg-black transition-all active:scale-[0.98] shadow-xl shadow-gray-200 flex items-center justify-center gap-3">
|
||||
View Ticket <i className="fas fa-ticket"></i>
|
||||
</button>
|
||||
)}
|
||||
{event.status !== 'Event Ongoing' && event.status !== 'Completed' && (
|
||||
<button onClick={() => setShowCancelConfirm(true)} className="w-full py-4 text-rose-500 font-black uppercase tracking-widest text-[9px] hover:bg-rose-50 rounded-2xl transition-all flex items-center justify-center gap-2 mt-2">
|
||||
<i className="fas fa-times-circle"></i> Cancel Registration
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : admissionStatus === 'OPEN' ? (
|
||||
<button onClick={() => setShowConfirm(true)} className="w-full py-5 rounded-[2rem] bg-[#1A202C] text-white font-black uppercase tracking-[0.3em] text-xs hover:bg-black transition-all active:scale-95 shadow-xl shadow-gray-200">
|
||||
Get Tickets
|
||||
</button>
|
||||
) : (
|
||||
<button disabled className="w-full py-5 rounded-[2rem] bg-rose-50 border border-rose-100 text-rose-400 font-black uppercase tracking-[0.3em] text-xs cursor-not-allowed">
|
||||
{admissionStatus === 'EVENT_ENDED'
|
||||
? 'EVENT ENDED'
|
||||
: admissionStatus === 'DEPT_FULL'
|
||||
? 'THE SEATS ARE FULL'
|
||||
: admissionStatus === 'DEADLINE_PASSED'
|
||||
? 'Deadline Over'
|
||||
: admissionStatus === 'SECTION_FULL'
|
||||
? 'SEC SEATS FULL'
|
||||
: admissionStatus === 'SECTION_NOT_ALLOWED'
|
||||
? 'SEC RESTRICTED'
|
||||
: 'Event Full'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showConfirm && (
|
||||
<Portal>
|
||||
<div className="fixed inset-0 z-[10000] flex items-center justify-center p-6 bg-slate-900/60 backdrop-blur-sm">
|
||||
<div className="bg-white rounded-[3rem] w-full max-w-sm p-12 text-center shadow-2xl animate-in zoom-in-95">
|
||||
<div className={`w-20 h-20 ${event.isTeamEvent ? 'bg-orange-50 text-orange-500' : 'bg-blue-50 text-blue-500'} rounded-full flex items-center justify-center text-3xl mx-auto mb-8`}>
|
||||
<i className={`fas ${event.isTeamEvent ? 'fa-users' : 'fa-ticket'}`}></i>
|
||||
</div>
|
||||
<h3 className="text-2xl font-black text-[#1A202C] uppercase mb-4">
|
||||
{event.isTeamEvent ? 'Team Registration' : 'Confirm Pass?'}
|
||||
</h3>
|
||||
|
||||
{event.isTeamEvent ? (
|
||||
<div className="space-y-6 mb-10">
|
||||
<div className="bg-orange-50 border border-orange-100 rounded-2xl p-6 text-left">
|
||||
<p className="text-[10px] font-black text-orange-600 uppercase tracking-widest mb-3 flex items-center gap-2">
|
||||
<i className="fas fa-exclamation-triangle"></i> Important Notice
|
||||
</p>
|
||||
<p className="text-xs text-orange-800 font-bold leading-relaxed lowercase first-letter:uppercase">
|
||||
This is a <span className="underline">team-based event</span>. You are about to register as an individual, after which you must <span className="underline">create or join a team</span> in the registrations section to participate.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="bg-slate-50 border border-slate-100 rounded-xl p-3">
|
||||
<span className="block text-[8px] font-black text-gray-400 uppercase tracking-widest mb-1">Max Size</span>
|
||||
<span className="text-xs font-black text-slate-900">{event.teamSizeLimit || '∞'} Members</span>
|
||||
</div>
|
||||
<div className="bg-slate-50 border border-slate-100 rounded-xl p-3">
|
||||
<span className="block text-[8px] font-black text-gray-400 uppercase tracking-widest mb-1">Type</span>
|
||||
<span className="text-xs font-black text-slate-900">{event.teamComposition === 'INTER_DEPT' ? 'Inter-Dept' : 'Mixed'}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-gray-500 text-[10px] font-bold uppercase tracking-widest">
|
||||
Proceed with registration for <span className="text-slate-900 underline">"{event.title}"</span>?
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-gray-500 text-sm mb-10 lowercase first-letter:uppercase">Register for <span className="font-bold">"{event.title}"</span>?</p>
|
||||
)}
|
||||
|
||||
<div className="flex gap-4">
|
||||
<button onClick={() => setShowConfirm(false)} className="flex-1 py-4 bg-gray-100 text-gray-500 rounded-2xl font-black uppercase tracking-widest text-[10px] hover:bg-gray-200 transition-all">Cancel</button>
|
||||
<button onClick={() => { onToggle(); setShowConfirm(false); }} className={`flex-1 py-4 ${event.isTeamEvent ? 'bg-orange-500 shadow-orange-200' : 'bg-[#1A202C] shadow-gray-200'} text-white rounded-2xl font-black uppercase tracking-widest text-[10px] shadow-lg active:scale-95 transition-all`}>
|
||||
{event.isTeamEvent ? 'I Understand' : 'Confirm'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Portal>
|
||||
)}
|
||||
|
||||
{showCancelConfirm && (
|
||||
<Portal>
|
||||
<div className="fixed inset-0 z-[10000] flex items-center justify-center p-6 bg-slate-900/60 backdrop-blur-sm">
|
||||
<div className="bg-white rounded-[3rem] w-full max-w-sm p-12 text-center shadow-2xl animate-in zoom-in-95">
|
||||
<div className="w-20 h-20 bg-rose-50 text-rose-500 rounded-full flex items-center justify-center text-3xl mx-auto mb-8"><i className="fas fa-calendar-xmark"></i></div>
|
||||
<h3 className="text-2xl font-black text-[#1A202C] uppercase mb-4">Cancel Booking?</h3>
|
||||
<p className="text-gray-500 text-sm mb-10">Are you sure you want to cancel your registration for <span className="font-bold">"{event.title}"</span>?</p>
|
||||
<div className="flex flex-col gap-3">
|
||||
<button onClick={() => { onToggle(); setShowCancelConfirm(false); }} className="w-full py-4 bg-rose-600 text-white rounded-2xl font-black uppercase tracking-widest text-[10px] shadow-lg shadow-rose-200">Yes, Cancel Registration</button>
|
||||
<button onClick={() => setShowCancelConfirm(false)} className="w-full py-4 bg-gray-100 text-gray-500 rounded-2xl font-black uppercase tracking-widest text-[10px]">Keep My Booking</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Portal>
|
||||
)}
|
||||
|
||||
{showTicket && (
|
||||
<Portal>
|
||||
<div className="fixed inset-0 z-[10000] flex items-center justify-center p-4 md:p-6 bg-slate-900/70 backdrop-blur-md animate-in fade-in duration-300">
|
||||
<div ref={ticketRef} className="relative bg-white rounded-[3rem] w-full max-w-md overflow-hidden shadow-[0_50px_100px_rgba(0,0,0,0.3)] animate-in zoom-in-95 duration-500 border border-white/20">
|
||||
|
||||
{/* Header - Now White Theme as Requested */}
|
||||
<div className="p-8 border-b border-gray-100 flex justify-between items-center bg-white">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-[3px] h-10 bg-[#f97316] rounded-full"></div>
|
||||
<img
|
||||
src="https://www.facultyplus.com/wp-content/uploads/2020/11/Rit-logo.png"
|
||||
alt="RIT Logo"
|
||||
className="h-10 w-auto object-contain"
|
||||
/>
|
||||
<div className="flex flex-col leading-none">
|
||||
<span className="text-[8px] font-black text-gray-400 uppercase tracking-[0.2em] mt-1">Secure Entry Pass</span>
|
||||
</div>
|
||||
</div>
|
||||
<button onClick={() => setShowTicket(false)} className="w-10 h-10 rounded-full bg-slate-50 flex items-center justify-center text-slate-400 hover:text-slate-900 transition-all"><i className="fas fa-times"></i></button>
|
||||
</div>
|
||||
|
||||
<div className="p-10">
|
||||
<div className="flex flex-col items-center mb-10">
|
||||
<div className="w-48 h-48 bg-gray-50 rounded-[2.5rem] flex items-center justify-center mb-8 p-6 border border-gray-100 shadow-inner relative group">
|
||||
<img
|
||||
src={`https://api.qrserver.com/v1/create-qr-code/?size=400x400&data=${encodeURIComponent(persistedTicket?.qr || verificationLink)}&color=1e293b`}
|
||||
alt="QR"
|
||||
className="w-full h-full object-contain"
|
||||
/>
|
||||
{persistedTicket?.id && (
|
||||
<div className="absolute -bottom-3 bg-white px-4 py-1 rounded-full border border-gray-200 shadow-sm">
|
||||
<span className="text-[8px] font-black text-gray-400 uppercase tracking-widest">{persistedTicket.id}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<h4 className="text-3xl font-black text-[#1A202C] text-center mb-2 tracking-tighter uppercase leading-none">{event.title}</h4>
|
||||
<div className="w-12 h-1 bg-[#f97316] rounded-full"></div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-y-8 gap-x-10 border-t border-dashed border-gray-200 pt-8 mb-10">
|
||||
<div>
|
||||
<span className="block text-[10px] font-black text-gray-400 uppercase tracking-widest mb-1">Attendee</span>
|
||||
<p className="text-sm font-black text-[#1A202C] truncate uppercase">{currentUserName}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="block text-[10px] font-black text-gray-400 uppercase tracking-widest mb-1">Year / Section</span>
|
||||
<p className="text-sm font-black text-[#1A202C] truncate uppercase">
|
||||
{registration?.year || userYear || 'N/A'} - {registration?.section || userSection || 'N/A'}
|
||||
</p>
|
||||
</div>
|
||||
{event.isTeamEvent && registration?.team_name && (
|
||||
<div className="col-span-2">
|
||||
<span className="block text-[10px] font-black text-gray-400 uppercase tracking-widest mb-1">Team Identity</span>
|
||||
<p className="text-sm font-black text-[#f97316] uppercase tracking-tight">{registration.team_name}</p>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<span className="block text-[10px] font-black text-gray-400 uppercase tracking-widest mb-1">Event Date</span>
|
||||
<p className="text-sm font-black text-[#1A202C] truncate uppercase">{event.date.split(',')[0]}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="block text-[10px] font-black text-gray-400 uppercase tracking-widest mb-1">Dept</span>
|
||||
<p className="text-sm font-black text-[#1A202C] truncate uppercase">{registration?.dept || userDept || 'Student'}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<button
|
||||
onClick={async () => {
|
||||
setIsSaving(true);
|
||||
await handleDownload();
|
||||
|
||||
// Handle bucket storage as requested: "once the student got the ticket then it must store in that bucket"
|
||||
if (ticketRef.current && registration?.id) {
|
||||
try {
|
||||
const dataUrl = await toPng(ticketRef.current, { quality: 1, backgroundColor: '#ffffff' });
|
||||
const fileName = `Ticket_${registration.id}.png`;
|
||||
// Using 'ticket_views' bucket created in DB migration
|
||||
const { uploadToSupabase: upload } = await import('../supabase');
|
||||
await upload(dataUrl, fileName, 'ticket_views');
|
||||
} catch (err) {
|
||||
console.error("Auto-archiving to bucket failed:", err);
|
||||
}
|
||||
}
|
||||
setIsSaving(false);
|
||||
}}
|
||||
disabled={isDownloading || isSaving}
|
||||
className="w-full py-5 bg-[#1A202C] text-white rounded-[2rem] font-black uppercase text-xs tracking-[0.3em] flex items-center justify-center gap-4 hover:bg-black transition-all shadow-xl shadow-gray-200 disabled:opacity-50"
|
||||
>
|
||||
{isDownloading || isSaving ? <><i className="fas fa-spinner fa-spin"></i> Processing...</> : <><i className="fas fa-download"></i> Save Pass</>}
|
||||
</button>
|
||||
<p className="text-[8px] text-center font-bold text-gray-400 uppercase tracking-widest">Digital Ticket ID: {persistedTicket?.id || 'GENERATING...'}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-[#f97316] h-3 w-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
</Portal>
|
||||
)}
|
||||
|
||||
{showSummary && (
|
||||
<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">
|
||||
<button
|
||||
onClick={() => setShowSummary(false)}
|
||||
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"
|
||||
>
|
||||
<i className="fas fa-times text-sm"></i>
|
||||
</button>
|
||||
|
||||
<div className="p-8 overflow-y-auto flex-1 custom-scrollbar">
|
||||
<div className="flex items-center gap-4 mb-8 pb-6 border-b border-gray-100 pr-8">
|
||||
{clubInfo && (
|
||||
<img src={clubInfo.image} className="w-12 h-12 rounded-full object-cover border border-slate-200 shadow-sm" alt="" />
|
||||
)}
|
||||
<div>
|
||||
<span className="block text-[10px] font-black text-[#f97316] uppercase tracking-[0.2em] mb-1">{event.club || 'Organized by'}</span>
|
||||
<span className="text-[10px] font-bold text-gray-500 uppercase tracking-widest flex items-center gap-2">
|
||||
<i className="fas fa-user text-[9px]"></i> {event.coordinator}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6 pb-6 mt-6 border-t border-gray-100 pt-6">
|
||||
<div className="flex items-center gap-2 mb-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>
|
||||
</div>
|
||||
<div className="relative pt-2">
|
||||
<i className="fas fa-quote-right absolute top-0 right-0 text-5xl text-slate-50 pointer-events-none -z-10"></i>
|
||||
<p className="text-[14px] text-slate-600 leading-relaxed whitespace-pre-wrap font-medium">
|
||||
{event.event_summary || "Details for this session will be provided soon."}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 shrink-0 border-t border-gray-100 pt-6">
|
||||
<button
|
||||
onClick={() => setShowSummary(false)}
|
||||
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"
|
||||
>
|
||||
Close Summary
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Portal>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EventCard;
|
||||
126
RIT-EVENT-MANAGEMENT--main/components/EventList.tsx
Normal file
126
RIT-EVENT-MANAGEMENT--main/components/EventList.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Event } from '../types';
|
||||
import EventCard from './EventCard';
|
||||
import CategoryGrid from './CategoryGrid';
|
||||
import DomainSelection from './DomainSelection';
|
||||
import EventsHeroSlider from './EventsHeroSlider';
|
||||
|
||||
interface EventListProps {
|
||||
events: Event[];
|
||||
selectedCategory: string | null;
|
||||
bookedEventIds: string[];
|
||||
onToggleBooking: (id: string) => void;
|
||||
onSelectCategory: (id: string | null) => void;
|
||||
onTrackStatus?: (event: Event) => void;
|
||||
currentUserName: string;
|
||||
userRole?: string;
|
||||
userRegistrations?: any[];
|
||||
}
|
||||
const EventList: React.FC<EventListProps> = ({
|
||||
events,
|
||||
selectedCategory,
|
||||
bookedEventIds,
|
||||
onToggleBooking,
|
||||
onSelectCategory,
|
||||
onTrackStatus,
|
||||
currentUserName,
|
||||
userRole,
|
||||
userRegistrations = []
|
||||
}) => {
|
||||
const [selectedDomain, setSelectedDomain] = useState<string | null>(null);
|
||||
const [selectedDomainName, setSelectedDomainName] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedDomain(null);
|
||||
setSelectedDomainName(null);
|
||||
}, [selectedCategory]);
|
||||
|
||||
if (!selectedCategory) {
|
||||
const hasActiveEvents = events.some(e => e.status !== 'Completed');
|
||||
return (
|
||||
<div className={!hasActiveEvents ? 'pt-32' : ''}>
|
||||
<EventsHeroSlider events={events} />
|
||||
<CategoryGrid onSelectCategory={onSelectCategory} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!selectedDomain) {
|
||||
return (
|
||||
<DomainSelection
|
||||
category={selectedCategory}
|
||||
onSelectDomain={(id, name) => {
|
||||
setSelectedDomain(id);
|
||||
if (name) setSelectedDomainName(name);
|
||||
}}
|
||||
onBack={() => onSelectCategory(null)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let filteredEvents = events;
|
||||
if (selectedCategory !== 'ALL') {
|
||||
filteredEvents = filteredEvents.filter(e => e.category === selectedCategory);
|
||||
}
|
||||
if (selectedDomain !== 'ALL') {
|
||||
filteredEvents = filteredEvents.filter(e => e.domain === selectedDomain);
|
||||
}
|
||||
|
||||
const categoryName = selectedCategory === 'ALL' ? 'All' :
|
||||
selectedCategory.charAt(0) + selectedCategory.slice(1).toLowerCase();
|
||||
const domainName = selectedDomainName || (selectedDomain === 'ALL' ? 'All Domains' : selectedDomain);
|
||||
|
||||
return (
|
||||
<div className="pt-40 pb-16 px-6 md:px-12 lg:px-24 animate-in fade-in duration-500">
|
||||
<button
|
||||
onClick={() => setSelectedDomain(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>
|
||||
BACK TO DOMAINS
|
||||
</button>
|
||||
|
||||
<div className="flex flex-col md:flex-row md:items-end justify-between mb-12 gap-6">
|
||||
<div>
|
||||
<h1 className="text-4xl md:text-5xl font-black text-[#1A202C] tracking-tight mb-2">
|
||||
{domainName}
|
||||
</h1>
|
||||
<p className="text-[#f97316] font-bold text-sm tracking-[0.2em] uppercase">
|
||||
Exploring {categoryName} Category
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-gray-400 font-medium">Showing {filteredEvents.length} events found</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-10">
|
||||
{filteredEvents.map(event => {
|
||||
const reg = userRegistrations.find(r => String(r.event_id) === String(event.id));
|
||||
return (
|
||||
<EventCard
|
||||
key={event.id}
|
||||
event={event}
|
||||
isBooked={bookedEventIds.includes(event.id)}
|
||||
onToggle={() => onToggleBooking(event.id)}
|
||||
onTrackStatus={onTrackStatus}
|
||||
currentUserName={currentUserName}
|
||||
userRole={userRole}
|
||||
registration={reg}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{filteredEvents.length === 0 && (
|
||||
<div className="py-24 text-center bg-white rounded-[40px] shadow-xl shadow-gray-900/10 border border-gray-200">
|
||||
<div className="w-20 h-20 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||
<i className="fas fa-calendar-times text-gray-300 text-3xl"></i>
|
||||
</div>
|
||||
<p className="text-gray-400 text-xl font-medium">No events found in this domain yet.</p>
|
||||
<button onClick={() => setSelectedDomain(null)} className="mt-6 px-6 py-2 bg-[#1A202C] text-white rounded-full text-xs font-bold uppercase tracking-widest hover:bg-black transition-colors">Try another domain</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EventList;
|
||||
179
RIT-EVENT-MANAGEMENT--main/components/EventsHeroSlider.tsx
Normal file
179
RIT-EVENT-MANAGEMENT--main/components/EventsHeroSlider.tsx
Normal file
@@ -0,0 +1,179 @@
|
||||
import React, { useState, useEffect, useMemo, useCallback } from 'react';
|
||||
import { Event } from '../types';
|
||||
|
||||
interface EventsHeroSliderProps {
|
||||
events: Event[];
|
||||
}
|
||||
|
||||
const EventsHeroSlider: React.FC<EventsHeroSliderProps> = ({ events }) => {
|
||||
const sortedEvents = useMemo(() => {
|
||||
return [...events]
|
||||
.filter(e => e.status !== 'Completed')
|
||||
.sort((a, b) => {
|
||||
return new Date(a.date).getTime() - new Date(b.date).getTime();
|
||||
});
|
||||
}, [events]);
|
||||
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
const [timeLeft, setTimeLeft] = useState({ d: '00', h: '00', m: '00', s: '00' });
|
||||
|
||||
const nextSlide = useCallback(() => {
|
||||
setCurrentIndex((prev) => (prev + 1) % sortedEvents.length);
|
||||
}, [sortedEvents.length]);
|
||||
|
||||
const prevSlide = useCallback(() => {
|
||||
setCurrentIndex((prev) => (prev - 1 + sortedEvents.length) % sortedEvents.length);
|
||||
}, [sortedEvents.length]);
|
||||
|
||||
useEffect(() => {
|
||||
if (sortedEvents.length === 0) return;
|
||||
const interval = setInterval(nextSlide, 5000);
|
||||
return () => clearInterval(interval);
|
||||
}, [sortedEvents.length, nextSlide]);
|
||||
|
||||
const updateTimer = useCallback(() => {
|
||||
const activeEvent = sortedEvents[currentIndex];
|
||||
if (!activeEvent) {
|
||||
setTimeLeft({ d: '00', h: '00', m: '00', s: '00' });
|
||||
return;
|
||||
}
|
||||
|
||||
const targetDate = new Date(activeEvent.date).getTime();
|
||||
const now = new Date().getTime();
|
||||
const distance = targetDate - now;
|
||||
|
||||
if (isNaN(targetDate) || distance < 0) {
|
||||
setTimeLeft({ d: '00', h: '00', m: '00', s: '00' });
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeLeft(prev => {
|
||||
const newD = Math.floor(distance / (1000 * 60 * 60 * 24)).toString().padStart(2, '0');
|
||||
const newH = Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)).toString().padStart(2, '0');
|
||||
const newM = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60)).toString().padStart(2, '0');
|
||||
const newS = Math.floor((distance % (1000 * 60)) / 1000).toString().padStart(2, '0');
|
||||
|
||||
if (prev.d === newD && prev.h === newH && prev.m === newM && prev.s === newS) {
|
||||
return prev;
|
||||
}
|
||||
return { d: newD, h: newH, m: newM, s: newS };
|
||||
});
|
||||
}, [sortedEvents, currentIndex]);
|
||||
|
||||
useEffect(() => {
|
||||
updateTimer();
|
||||
const timerId = setInterval(updateTimer, 1000);
|
||||
return () => clearInterval(timerId);
|
||||
}, [updateTimer]);
|
||||
|
||||
if (sortedEvents.length === 0) return null;
|
||||
|
||||
const activeEvent = sortedEvents[currentIndex];
|
||||
|
||||
return (
|
||||
<div className="relative w-full h-screen bg-[#1A202C] overflow-hidden group">
|
||||
{/* Background Images & Content */}
|
||||
{sortedEvents.map((event, idx) => {
|
||||
const isActive = idx === currentIndex;
|
||||
return (
|
||||
<div
|
||||
key={event.id}
|
||||
className={`absolute inset-0 transition-opacity duration-1000 ease-in-out ${isActive ? 'opacity-100 z-10' : 'opacity-0 z-0'}`}
|
||||
>
|
||||
{/* Background Image */}
|
||||
<div className="absolute inset-0 bg-black">
|
||||
<img
|
||||
src={event.image}
|
||||
alt={event.title}
|
||||
className={`w-full h-full object-cover opacity-50 transition-transform duration-[5000ms] ${isActive ? 'scale-100' : 'scale-105'}`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Top Right Location Button */}
|
||||
<div className="absolute top-24 right-12 z-20">
|
||||
<div className="bg-white px-6 py-3 rounded-full flex items-center gap-3 shadow-2xl shadow-gray-200/50">
|
||||
<i className="fas fa-location-dot text-[#f97316]"></i>
|
||||
<span className="text-[#1A202C] text-xs font-bold tracking-widest uppercase">{event.location}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="absolute inset-0 z-10 flex flex-col justify-center pb-[15vh] px-12 md:px-24">
|
||||
<div className={`max-w-5xl transition-all duration-1000 transform ${isActive ? 'translate-y-0 opacity-100' : 'translate-y-8 opacity-0'}`}>
|
||||
<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>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h1 className="text-white text-5xl md:text-[6rem] lg:text-[8rem] font-black leading-[0.9] tracking-tighter uppercase break-words drop-shadow-lg">
|
||||
{event.title}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Countdown Timer Block (Slanted Orange Design) */}
|
||||
<div
|
||||
className="absolute bottom-0 left-0 z-20 bg-[#f97316] h-[140px] md:h-[160px] flex items-center px-8 md:px-16"
|
||||
style={{
|
||||
clipPath: 'polygon(0 0, 85% 0, 100% 100%, 0% 100%)',
|
||||
width: '100%',
|
||||
maxWidth: '800px'
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-6 md:gap-12 relative z-10 w-full pr-12 md:pr-24">
|
||||
<div className="flex flex-col items-center flex-1">
|
||||
<span className="text-5xl md:text-6xl font-black leading-none text-white tracking-tighter">{timeLeft.d}</span>
|
||||
<span className="text-[10px] md:text-xs font-bold uppercase tracking-widest mt-2 text-white">Days</span>
|
||||
</div>
|
||||
<div className="w-px h-16 bg-white/30"></div>
|
||||
<div className="flex flex-col items-center flex-1">
|
||||
<span className="text-5xl md:text-6xl font-black leading-none text-white tracking-tighter">{timeLeft.h}</span>
|
||||
<span className="text-[10px] md:text-xs font-bold uppercase tracking-widest mt-2 text-white">Hrs</span>
|
||||
</div>
|
||||
<div className="w-px h-16 bg-white/30"></div>
|
||||
<div className="flex flex-col items-center flex-1">
|
||||
<span className="text-5xl md:text-6xl font-black leading-none text-white tracking-tighter">{timeLeft.m}</span>
|
||||
<span className="text-[10px] md:text-xs font-bold uppercase tracking-widest mt-2 text-white">Min</span>
|
||||
</div>
|
||||
<div className="w-px h-16 bg-white/30"></div>
|
||||
<div className="flex flex-col items-center flex-1">
|
||||
<span className="text-5xl md:text-6xl font-black leading-none text-white tracking-tighter">{timeLeft.s}</span>
|
||||
<span className="text-[10px] md:text-xs font-bold uppercase tracking-widest mt-2 text-white">Sec</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Navigation Arrows */}
|
||||
<button
|
||||
onClick={prevSlide}
|
||||
className="absolute left-8 top-1/2 -translate-y-1/2 w-12 h-12 rounded-full bg-white/10 backdrop-blur-md flex items-center justify-center text-white/50 hover:text-white hover:bg-white/20 transition-all duration-300 z-20 opacity-0 group-hover:opacity-100 hover:scale-110 border border-white/10"
|
||||
>
|
||||
<i className="fas fa-chevron-left text-lg"></i>
|
||||
</button>
|
||||
<button
|
||||
onClick={nextSlide}
|
||||
className="absolute right-8 top-1/2 -translate-y-1/2 w-12 h-12 rounded-full bg-white/10 backdrop-blur-md flex items-center justify-center text-white/50 hover:text-white hover:bg-white/20 transition-all duration-300 z-20 opacity-0 group-hover:opacity-100 hover:scale-110 border border-white/10"
|
||||
>
|
||||
<i className="fas fa-chevron-right text-lg"></i>
|
||||
</button>
|
||||
|
||||
{/* Slider Dots */}
|
||||
<div className="absolute bottom-12 right-12 z-20 flex gap-3">
|
||||
{sortedEvents.map((_, idx) => (
|
||||
<button
|
||||
key={idx}
|
||||
onClick={() => setCurrentIndex(idx)}
|
||||
className={`h-1 transition-all duration-500 ${currentIndex === idx ? 'w-12 bg-[#f97316]' : 'w-6 bg-gray-200 hover:bg-gray-400'}`}
|
||||
></button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EventsHeroSlider;
|
||||
538
RIT-EVENT-MANAGEMENT--main/components/FacultyAttendanceView.tsx
Normal file
538
RIT-EVENT-MANAGEMENT--main/components/FacultyAttendanceView.tsx
Normal file
@@ -0,0 +1,538 @@
|
||||
import React, { useState, useMemo, useEffect } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { Event } from '../types';
|
||||
import { supabase } from '../supabase';
|
||||
|
||||
interface FacultyAttendanceViewProps {
|
||||
events: Event[];
|
||||
onShowToast: (msg: string) => void;
|
||||
localRegistrations?: any[];
|
||||
currentUserId?: string;
|
||||
}
|
||||
|
||||
interface FilterState {
|
||||
depts: string[];
|
||||
years: string[];
|
||||
}
|
||||
|
||||
const FacultyAttendanceView: React.FC<FacultyAttendanceViewProps> = ({ events, onShowToast, localRegistrations = [], currentUserId }) => {
|
||||
const [selectedEventId, setSelectedEventId] = useState<string | null>(null);
|
||||
const [showFilters, setShowFilters] = useState(false);
|
||||
const [activeFilters, setActiveFilters] = useState<FilterState>({ depts: [], years: [] });
|
||||
const [activeTab, setActiveTab] = useState<'DEPT' | 'YEAR'>('DEPT');
|
||||
|
||||
// Track attendance in local state for the current session
|
||||
const [attendanceMap, setAttendanceMap] = useState<Record<string, boolean>>({});
|
||||
const [selectedDay, setSelectedDay] = useState<string>('Day 1');
|
||||
const [selectedBatch, setSelectedBatch] = useState<string>('');
|
||||
const [isSyncing, setIsSyncing] = useState(false);
|
||||
const [isFinalized, setIsFinalized] = useState(false);
|
||||
|
||||
const sessionLabel = useMemo(() => {
|
||||
if (!selectedBatch) return selectedDay;
|
||||
return `${selectedDay} - ${selectedBatch}`;
|
||||
}, [selectedDay, selectedBatch]);
|
||||
|
||||
// Attendance markers logic
|
||||
useEffect(() => {
|
||||
if (selectedEventId && sessionLabel) {
|
||||
const fetchAttendance = async () => {
|
||||
setIsSyncing(true);
|
||||
const { data, error } = await supabase
|
||||
.from('attendance_records')
|
||||
.select('registration_id, is_present')
|
||||
.eq('event_id', selectedEventId)
|
||||
.or(`day_label.eq."${selectedDay}",session_label.eq."${sessionLabel}"`)
|
||||
.eq('batch_label', selectedBatch || '');
|
||||
|
||||
if (data && !error) {
|
||||
const map: Record<string, boolean> = {};
|
||||
data.forEach((rec: any) => {
|
||||
map[rec.registration_id] = rec.is_present;
|
||||
});
|
||||
setAttendanceMap(map);
|
||||
setIsFinalized(data.length > 0);
|
||||
} else {
|
||||
setAttendanceMap({});
|
||||
setIsFinalized(false);
|
||||
}
|
||||
setIsSyncing(false);
|
||||
};
|
||||
fetchAttendance();
|
||||
}
|
||||
}, [selectedEventId, sessionLabel]);
|
||||
|
||||
const toggleAttendance = (id: string) => {
|
||||
setAttendanceMap(prev => ({ ...prev, [id]: !prev[id] }));
|
||||
};
|
||||
|
||||
const handleFinalize = async () => {
|
||||
if (!selectedEventId) return;
|
||||
|
||||
if (selectedEvent?.created_by !== currentUserId) {
|
||||
alert("Unauthorized: Only the event creator can finalize attendance.");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSyncing(true);
|
||||
try {
|
||||
// Upsert attendance records
|
||||
const records = currentRoster.map(s => ({
|
||||
event_id: selectedEventId,
|
||||
registration_id: s.id,
|
||||
session_label: sessionLabel, // Keep for backward compatibility
|
||||
day_label: selectedDay,
|
||||
batch_label: selectedBatch || '',
|
||||
is_present: !!attendanceMap[s.id],
|
||||
marked_at: new Date().toISOString()
|
||||
}));
|
||||
|
||||
const { error } = await supabase
|
||||
.from('attendance_records')
|
||||
.upsert(records, { onConflict: 'registration_id,day_label,batch_label' });
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
setIsFinalized(true);
|
||||
onShowToast(`Attendance for ${sessionLabel} finalized and synced.`);
|
||||
} catch (err: any) {
|
||||
console.error("Failed to sync attendance:", err);
|
||||
alert("Failed to sync attendance. Please try again.");
|
||||
} finally {
|
||||
setIsSyncing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownloadExcel = () => {
|
||||
if (!selectedEvent || filteredRoster.length === 0) return;
|
||||
|
||||
const titleRow = [`EVENT: ${selectedEvent.title}`];
|
||||
const sessionRow = [`SESSION: ${selectedDay}${selectedBatch ? ` - ${selectedBatch}` : ''}`];
|
||||
const dateRow = [`EXPORTED AT: ${new Date().toLocaleString()}`];
|
||||
const emptyRow = [''];
|
||||
|
||||
// Reg No formatting: Prefix with \t to prevent scientific notation in Excel
|
||||
const headers = ['Name', 'Registration No', 'Department', 'Year', 'Present', 'Absent'];
|
||||
const rows = filteredRoster.map(s => [
|
||||
s.name,
|
||||
`\t${s.roll}`,
|
||||
s.dept,
|
||||
s.year,
|
||||
s.present ? '1' : '0',
|
||||
s.present ? '0' : '1'
|
||||
]);
|
||||
|
||||
const csvContent = [
|
||||
titleRow.join(','),
|
||||
sessionRow.join(','),
|
||||
dateRow.join(','),
|
||||
emptyRow.join(','),
|
||||
headers.join(','),
|
||||
...rows.map(row => row.map(cell => `"${cell}"`).join(','))
|
||||
].join('\n');
|
||||
|
||||
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.setAttribute('href', url);
|
||||
const sanitizedTitle = selectedEvent.title.replace(/[^a-z0-9]/gi, '_').toLowerCase();
|
||||
link.setAttribute('download', `${sanitizedTitle}_attendance_${selectedDay}_${selectedBatch || 'full'}.csv`);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
};
|
||||
|
||||
const selectedEvent = events.find(e => e.id === selectedEventId);
|
||||
|
||||
// Compute the current roster based on registrations in the database for the selected event
|
||||
const currentRoster = useMemo(() => {
|
||||
if (!selectedEventId) return [];
|
||||
return localRegistrations
|
||||
.filter(reg => reg.event_id === selectedEventId)
|
||||
.map(reg => ({
|
||||
id: reg.id,
|
||||
userId: reg.user_id,
|
||||
name: reg.user_name || 'Student',
|
||||
roll: reg.reg_no || 'N/A',
|
||||
dept: reg.dept || 'N/A',
|
||||
year: reg.year || 'N/A',
|
||||
college: reg.college,
|
||||
present: !!attendanceMap[reg.id]
|
||||
}));
|
||||
}, [selectedEventId, localRegistrations, attendanceMap]);
|
||||
|
||||
// Derive unique values for filters from the dynamic roster
|
||||
const filterOptions = useMemo(() => {
|
||||
return {
|
||||
depts: Array.from(new Set(currentRoster.map(s => s.dept))).sort(),
|
||||
years: Array.from(new Set(currentRoster.map(s => s.year))).sort()
|
||||
};
|
||||
}, [currentRoster]);
|
||||
|
||||
// Apply filters to roster
|
||||
const filteredRoster = useMemo(() => {
|
||||
return currentRoster.filter(student => {
|
||||
const deptMatch = activeFilters.depts.length === 0 || activeFilters.depts.includes(student.dept);
|
||||
const yearMatch = activeFilters.years.length === 0 || activeFilters.years.includes(student.year);
|
||||
|
||||
const isExternal = student.college && student.college !== 'RAJALAKSHMI INSTITUTE OF TECHNOLOGY' && student.college !== 'Rajalakshmi Institute of Technology';
|
||||
|
||||
return deptMatch && yearMatch && !isExternal;
|
||||
});
|
||||
}, [currentRoster, activeFilters]);
|
||||
|
||||
const toggleFilterValue = (category: 'depts' | 'years', value: string) => {
|
||||
setActiveFilters(prev => {
|
||||
const current = prev[category];
|
||||
const next = current.includes(value)
|
||||
? current.filter(v => v !== value)
|
||||
: [...current, value];
|
||||
return { ...prev, [category]: next };
|
||||
});
|
||||
};
|
||||
|
||||
const clearAllFilters = () => {
|
||||
setActiveFilters({ depts: [], years: [] });
|
||||
};
|
||||
|
||||
const totalActiveFilters = activeFilters.depts.length + activeFilters.years.length;
|
||||
|
||||
return (
|
||||
<div className="animate-in slide-in-from-bottom-10 duration-500">
|
||||
{!selectedEventId ? (
|
||||
<>
|
||||
<div className="mb-12 border-b border-slate-200 pb-8">
|
||||
<h3 className="text-4xl font-black tracking-tighter uppercase mb-2 text-slate-900">SYSTEM <span className="text-[#004a99]">ATTENDANCE</span></h3>
|
||||
<p className="text-[10px] font-black text-slate-500 uppercase tracking-[0.4em]">Select an Event to Mark Roster</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{events.map((event) => (
|
||||
<div key={event.id} onClick={() => setSelectedEventId(event.id)} className="bg-white border border-slate-200 rounded-[2.5rem] overflow-hidden group cursor-pointer hover:border-blue-300 transition-all flex flex-col h-full shadow-sm hover:shadow-md">
|
||||
<div className="h-48 relative overflow-hidden bg-slate-100">
|
||||
<img src={event.image} alt={event.title} className="w-full h-full object-cover opacity-80 group-hover:opacity-100 transition-all duration-700 group-hover:scale-105" />
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent"></div>
|
||||
<div className="absolute top-6 right-6 flex flex-col items-end gap-2">
|
||||
<div className="bg-white/90 backdrop-blur-md border border-slate-200 px-4 py-1.5 rounded-full text-[9px] font-black uppercase tracking-widest text-slate-700 shadow-sm">{event.category}</div>
|
||||
<div className={`px-3 py-1 rounded-md text-[8px] font-black uppercase tracking-widest border ${event.pricingType === 'PAID' ? 'bg-rose-50 border-rose-200 text-rose-600' : 'bg-emerald-50 border-emerald-200 text-emerald-600'}`}>{event.pricingType}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-8 flex flex-col flex-1">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-blue-500"></span>
|
||||
<span className="text-[9px] font-black text-slate-500 uppercase tracking-[0.2em]">{event.domain}</span>
|
||||
</div>
|
||||
<h4 className="text-xl font-black uppercase mb-2 tracking-tight text-slate-900 group-hover:text-[#004a99] transition-colors line-clamp-1">{event.title}</h4>
|
||||
<p className="text-[10px] font-bold text-slate-500 uppercase mb-6 tracking-widest">Coord: {event.coordinator}</p>
|
||||
<div className="mt-auto flex items-center justify-between border-t border-slate-100 pt-6">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[8px] font-black text-slate-400 uppercase tracking-widest mb-1">Registered</span>
|
||||
<span className="text-xs font-bold text-slate-700 uppercase">{localRegistrations.filter(r => r.event_id === event.id).length} Students</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-[#004a99] font-black text-[10px] uppercase tracking-widest group-hover:translate-x-1 transition-transform">
|
||||
{event.created_by === currentUserId ? "Mark Roster" : "View Roster"} <i className="fas fa-chevron-right text-[8px]"></i>
|
||||
</div>
|
||||
</div>
|
||||
{event.created_by !== currentUserId && (
|
||||
<div className="absolute top-4 left-4 bg-slate-900/10 backdrop-blur-md px-3 py-1 rounded-full text-[7px] font-black uppercase tracking-widest text-[#004a99] flex items-center gap-1 border border-white/20">
|
||||
<i className="fas fa-eye"></i> Read Only
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="max-w-5xl mx-auto pb-20">
|
||||
<button onClick={() => { setSelectedEventId(null); clearAllFilters(); }} className="flex items-center gap-2 text-[#004a99] font-black uppercase tracking-widest mb-10 group hover:translate-x-[-5px] transition-transform text-xs"><i className="fas fa-arrow-left"></i> Back to Event List</button>
|
||||
|
||||
<div className="bg-white border border-slate-200 rounded-[3rem] overflow-hidden shadow-sm relative">
|
||||
<div className="p-10 border-b border-slate-100 bg-slate-50">
|
||||
<div className="flex flex-col md:flex-row justify-between items-center gap-8 mb-8">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center flex-wrap gap-3 mb-4">
|
||||
<span className="bg-blue-50 text-[#004a99] px-3 py-1 rounded-full text-[8px] font-black uppercase tracking-widest border border-blue-200">{selectedEvent?.category}</span>
|
||||
<span className={`px-3 py-1 rounded text-[8px] font-black uppercase tracking-widest border ${selectedEvent?.pricingType === 'PAID' ? 'bg-rose-50 border-rose-200 text-rose-600' : 'bg-emerald-50 border-emerald-200 text-emerald-600'}`}>{selectedEvent?.pricingType}</span>
|
||||
</div>
|
||||
<h4 className="text-4xl font-black uppercase tracking-tighter mb-2 text-slate-900">{selectedEvent?.title}</h4>
|
||||
<p className="text-[10px] font-bold text-[#004a99] uppercase tracking-[0.4em] flex items-center gap-2"><i className="fas fa-barcode"></i> SESSION ACTIVE</p>
|
||||
</div>
|
||||
|
||||
<div className="text-center md:text-right bg-white border border-slate-200 p-6 rounded-3xl min-w-[200px] shadow-sm">
|
||||
<span className="block text-[10px] font-black text-slate-400 uppercase tracking-widest mb-2">Marked Attendance</span>
|
||||
<span className="text-5xl font-black text-[#004a99] tabular-nums">{filteredRoster.filter(s => s.present).length}<span className="text-slate-300 mx-2">/</span><span className="text-slate-900">{filteredRoster.length}</span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center justify-end gap-6 pt-8 border-t border-slate-200/60 font-bold text-xs uppercase tracking-widest text-slate-400">
|
||||
<span>Filter Options</span>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
{/* Day Selector */}
|
||||
{(() => {
|
||||
// Derive unique days from schedule
|
||||
const uniqueDays = selectedEvent?.schedule
|
||||
? (Array.from(new Set(selectedEvent.schedule.map(s => s.day_idx))) as number[]).sort((a, b) => a - b)
|
||||
: [1];
|
||||
return uniqueDays.length > 1 ? (
|
||||
<select
|
||||
value={selectedDay}
|
||||
onChange={(e) => {
|
||||
setSelectedDay(e.target.value);
|
||||
setSelectedBatch(''); // Reset batch when day changes
|
||||
}}
|
||||
className="bg-white border border-slate-200 px-4 py-3 rounded-xl text-[9px] font-black uppercase tracking-widest text-[#004a99] outline-none shadow-sm focus:ring-2 focus:ring-blue-100 transition-all cursor-pointer min-w-[120px]"
|
||||
>
|
||||
{uniqueDays.map(dayIdx => (
|
||||
<option key={dayIdx} value={`Day ${dayIdx}`}>Day {dayIdx}</option>
|
||||
))}
|
||||
</select>
|
||||
) : (
|
||||
<div className="bg-slate-100 border border-slate-200 px-4 py-3 rounded-xl text-[9px] font-black uppercase tracking-widest text-slate-400 shadow-sm cursor-default">
|
||||
Day 1 Only
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* Batch Selector */}
|
||||
{(() => {
|
||||
const currentDayIdx = parseInt(selectedDay.split(' ')[1]) || 1;
|
||||
const dayBatches = selectedEvent?.schedule
|
||||
? selectedEvent.schedule.filter(s => s.day_idx === currentDayIdx)
|
||||
: [];
|
||||
|
||||
if (dayBatches.length > 0) {
|
||||
return (
|
||||
<select
|
||||
value={selectedBatch}
|
||||
onChange={(e) => setSelectedBatch(e.target.value)}
|
||||
className="bg-white border border-slate-200 px-4 py-3 rounded-xl text-[9px] font-black uppercase tracking-widest text-[#004a99] outline-none shadow-sm focus:ring-2 focus:ring-blue-100 transition-all cursor-pointer min-w-[120px]"
|
||||
>
|
||||
<option value="">Full Day</option>
|
||||
{dayBatches.map((s) => (
|
||||
<option key={s.id} value={`Batch ${s.batch_idx}`}>Batch {s.batch_idx}</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})()}
|
||||
|
||||
<button
|
||||
onClick={() => setShowFilters(true)}
|
||||
className="group flex items-center gap-3 bg-white border border-slate-200 px-6 py-3 rounded-xl hover:bg-slate-100 text-slate-700 transition-all relative shadow-sm"
|
||||
>
|
||||
<i className="fas fa-sliders-h text-xs text-[#004a99]"></i>
|
||||
<span className="text-[9px] font-black uppercase tracking-widest">Filters</span>
|
||||
{totalActiveFilters > 0 && (
|
||||
<span className="absolute -top-2 -right-2 w-5 h-5 bg-[#004a99] text-white text-[9px] font-black rounded-full flex items-center justify-center animate-bounce shadow-md">
|
||||
{totalActiveFilters}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto min-h-[400px]">
|
||||
<table className="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-slate-50/50 border-b border-slate-100">
|
||||
<th className="px-8 py-5 text-[9px] font-black text-slate-400 uppercase tracking-widest">#</th>
|
||||
<th className="px-8 py-5 text-[9px] font-black text-slate-400 uppercase tracking-widest">Student Details</th>
|
||||
<th className="px-8 py-5 text-[9px] font-black text-slate-400 uppercase tracking-widest">Academic Info</th>
|
||||
<th className="px-8 py-5 text-[9px] font-black text-slate-400 uppercase tracking-widest text-right">Roster Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-50 relative">
|
||||
{isSyncing && (
|
||||
<div className="absolute inset-0 bg-white/60 backdrop-blur-[1px] z-10 flex items-center justify-center">
|
||||
<div className="w-8 h-8 border-2 border-blue-100 border-t-[#004a99] rounded-full animate-spin"></div>
|
||||
</div>
|
||||
)}
|
||||
{filteredRoster.length > 0 ? filteredRoster.map((student, idx) => (
|
||||
<tr key={student.id} className={`group/row transition-all ${student.present ? 'bg-emerald-50/30' : 'hover:bg-slate-50/50'}`}>
|
||||
<td className="px-8 py-5">
|
||||
<span className={`text-[10px] font-black ${student.present ? 'text-emerald-500' : 'text-slate-300'}`}>{idx + 1}</span>
|
||||
</td>
|
||||
<td className="px-8 py-5">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className={`w-10 h-10 rounded-xl flex items-center justify-center font-black text-sm uppercase ${student.present ? 'bg-emerald-500 text-white shadow-lg shadow-emerald-500/20' : 'bg-slate-100 text-slate-400 border border-slate-200'}`}>
|
||||
{student.name.charAt(0)}
|
||||
</div>
|
||||
<div>
|
||||
<p className={`text-sm font-black uppercase tracking-tight ${student.present ? 'text-emerald-700' : 'text-slate-900'}`}>{student.name}</p>
|
||||
<p className="text-[9px] font-bold text-slate-400 uppercase tracking-widest mt-0.5">{student.roll}</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-8 py-5">
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<p className="text-[9px] font-black text-slate-700 uppercase tracking-wider">{student.dept}</p>
|
||||
<p className="text-[9px] font-black text-slate-400 uppercase tracking-widest">{student.year}</p>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-8 py-5 text-right">
|
||||
{student.present ? (
|
||||
<div className="flex items-center justify-center gap-2 py-3 bg-emerald-500 text-white rounded-xl text-[10px] font-black uppercase tracking-[0.2em] shadow-lg shadow-emerald-500/20">
|
||||
<i className="fas fa-check-circle"></i> PRESENT
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => selectedEvent?.created_by === currentUserId && !isFinalized && toggleAttendance(student.id)}
|
||||
disabled={selectedEvent?.created_by !== currentUserId || isFinalized}
|
||||
className={`w-full px-6 py-2.5 bg-white border border-slate-200 text-slate-400 ${(selectedEvent?.created_by === currentUserId && !isFinalized) ? 'hover:border-emerald-300 hover:text-emerald-600 hover:bg-emerald-50 cursor-pointer' : 'opacity-50 cursor-not-allowed'} rounded-xl font-black uppercase text-[8px] tracking-[0.2em] transition-all active:scale-95 shadow-sm`}
|
||||
>
|
||||
{selectedEvent?.created_by !== currentUserId ? "READ ONLY" : isFinalized ? "FINALIZED" : "MARK PRESENT"}
|
||||
</button>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
)) : (
|
||||
<tr>
|
||||
<td colSpan={4} className="py-20 text-center">
|
||||
<div className="flex flex-col items-center">
|
||||
<i className="fas fa-filter-circle-xmark text-slate-200 text-3xl mb-4"></i>
|
||||
<h5 className="text-xs font-black uppercase text-slate-400 tracking-widest">No matching students</h5>
|
||||
<button onClick={clearAllFilters} className="mt-2 text-[#004a99] font-black text-[9px] uppercase tracking-widest hover:underline">Reset Filters</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="p-10 bg-slate-50 border-t border-slate-200 flex flex-col md:flex-row items-center justify-between gap-6">
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-full bg-emerald-100 flex items-center justify-center"><i className="fas fa-cloud-check text-emerald-600"></i></div>
|
||||
<p className="text-[9px] font-black text-slate-500 uppercase tracking-widest leading-tight">Roster synced<br/>with database</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleDownloadExcel}
|
||||
className="px-6 py-3 bg-white border border-slate-200 rounded-xl text-slate-600 text-[10px] font-black uppercase tracking-widest hover:border-blue-300 hover:text-[#004a99] transition-all flex items-center gap-3 shadow-sm"
|
||||
>
|
||||
<i className="fas fa-file-excel"></i> Export CSV
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleFinalize}
|
||||
disabled={isSyncing || selectedEvent?.created_by !== currentUserId || isFinalized}
|
||||
className="w-full md:w-auto px-12 py-5 bg-[#004a99] text-white rounded-2xl font-black uppercase tracking-[0.3em] text-xs hover:bg-blue-800 transition-all shadow-md active:scale-95 flex items-center justify-center gap-4 disabled:opacity-50"
|
||||
>
|
||||
{isSyncing ? 'Syncing...' : (selectedEvent?.created_by !== currentUserId ? "Read Only View" : isFinalized ? <>Roster Locked <i className="fas fa-lock"></i></> : <>Finalize {sessionLabel} <i className="fas fa-cloud-arrow-up"></i></>)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showFilters && createPortal(
|
||||
<div className="fixed inset-0 z-[10000] flex justify-end">
|
||||
<div
|
||||
className="absolute inset-0 bg-slate-900/40 backdrop-blur-sm animate-in fade-in duration-300"
|
||||
onClick={() => setShowFilters(false)}
|
||||
/>
|
||||
|
||||
<div className="relative w-full max-w-md bg-white h-full shadow-2xl flex flex-col animate-in slide-in-from-right duration-500 border-l border-slate-200">
|
||||
<div className="p-8 border-b border-slate-100 flex justify-between items-center bg-slate-50">
|
||||
<div>
|
||||
<h4 className="text-xl font-black text-slate-900 uppercase tracking-tight">Refine Roster</h4>
|
||||
<p className="text-[9px] font-bold text-[#004a99] uppercase tracking-widest mt-1">Multi-Category Filtering</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowFilters(false)}
|
||||
className="w-10 h-10 rounded-full bg-white border border-slate-200 flex items-center justify-center text-slate-400 hover:text-slate-900 transition-all shadow-sm"
|
||||
>
|
||||
<i className="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 flex overflow-hidden">
|
||||
<div className="w-32 bg-slate-50/50 border-r border-slate-100">
|
||||
{[
|
||||
{ id: 'DEPT' as const, label: 'Department' },
|
||||
{ id: 'YEAR' as const, label: 'Year' }
|
||||
].map(tab => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
className={`w-full py-6 px-4 text-left relative transition-all ${activeTab === tab.id
|
||||
? 'bg-blue-50 text-blue-800'
|
||||
: 'text-slate-500 hover:text-slate-700'
|
||||
}`}
|
||||
>
|
||||
<span className="text-[10px] font-black uppercase tracking-widest">{tab.label}</span>
|
||||
{activeTab === tab.id && <div className="absolute right-0 top-0 bottom-0 w-1 bg-[#004a99]" />}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 p-8 overflow-y-auto custom-scrollbar">
|
||||
{activeTab === 'DEPT' ? (
|
||||
<div className="space-y-4">
|
||||
{filterOptions.depts.map(dept => (
|
||||
<label key={dept} className="flex items-center gap-4 group cursor-pointer" onClick={() => toggleFilterValue('depts', dept)}>
|
||||
<div
|
||||
className={`w-6 h-6 rounded-lg border-2 flex items-center justify-center transition-all ${activeFilters.depts.includes(dept)
|
||||
? 'bg-[#004a99] border-[#004a99] text-white shadow-sm'
|
||||
: 'border-slate-200 bg-white group-hover:border-blue-400'
|
||||
}`}
|
||||
>
|
||||
{activeFilters.depts.includes(dept) && <i className="fas fa-check text-[10px] text-white"></i>}
|
||||
</div>
|
||||
<span className={`text-xs font-black uppercase tracking-widest transition-colors ${activeFilters.depts.includes(dept) ? 'text-slate-900' : 'text-slate-500'
|
||||
}`}>
|
||||
{dept}
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{filterOptions.years.map(year => (
|
||||
<label key={year} className="flex items-center gap-4 group cursor-pointer" onClick={() => toggleFilterValue('years', year)}>
|
||||
<div
|
||||
className={`w-6 h-6 rounded-lg border-2 flex items-center justify-center transition-all ${activeFilters.years.includes(year)
|
||||
? 'bg-[#004a99] border-[#004a99] text-white shadow-sm'
|
||||
: 'border-slate-200 bg-white group-hover:border-blue-400'
|
||||
}`}
|
||||
>
|
||||
{activeFilters.years.includes(year) && <i className="fas fa-check text-[10px] text-white"></i>}
|
||||
</div>
|
||||
<span className={`text-xs font-black uppercase tracking-widest transition-colors ${activeFilters.years.includes(year) ? 'text-slate-900' : 'text-slate-500'
|
||||
}`}>
|
||||
{year}
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-8 border-t border-slate-100 bg-slate-50 flex gap-4">
|
||||
<button
|
||||
onClick={clearAllFilters}
|
||||
className="flex-1 py-4 bg-white border border-slate-200 rounded-2xl text-[10px] font-black text-slate-500 uppercase tracking-widest hover:bg-slate-100 transition-all shadow-sm"
|
||||
>
|
||||
Clear All
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowFilters(false)}
|
||||
className="flex-1 py-4 bg-[#004a99] text-white rounded-2xl text-[10px] font-black uppercase tracking-widest hover:bg-blue-800 transition-all shadow-md active:scale-95"
|
||||
>
|
||||
Apply Filter
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FacultyAttendanceView;
|
||||
893
RIT-EVENT-MANAGEMENT--main/components/FacultyDashboard.tsx
Normal file
893
RIT-EVENT-MANAGEMENT--main/components/FacultyDashboard.tsx
Normal file
@@ -0,0 +1,893 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { FacultyView, Event, Announcement } from '../types';
|
||||
import FacultyProfileView from './FacultyProfileView';
|
||||
import FacultyNotifications from './FacultyNotifications';
|
||||
import AdminStatusTrackerView from './AdminStatusTrackerView';
|
||||
import UserManagementView from './UserManagementView';
|
||||
import FacultyParticipantsView from './FacultyParticipantsView';
|
||||
import { supabase } from '../supabase';
|
||||
import { generateEventDetailsPDF } from '../utils/pdfGenerator';
|
||||
|
||||
interface FacultyDashboardProps {
|
||||
onLogout: () => void;
|
||||
events: Event[];
|
||||
announcements: Announcement[];
|
||||
studentDocuments: Record<string, string>;
|
||||
onApproveCertificate: (regId: string) => Promise<void>;
|
||||
localRegistrations?: any[];
|
||||
onViewStudentHub?: () => void;
|
||||
currentUserName?: string;
|
||||
currentUserDept?: string;
|
||||
currentUserEmail?: string;
|
||||
onUpdateEvent: (event: Event) => void;
|
||||
specialEvents?: any[];
|
||||
onUpdateSpecialEvent?: (se: any) => Promise<void>;
|
||||
}
|
||||
|
||||
// Extend FacultyView to include STATUS_TRACKER, USER_MANAGEMENT, PARTICIPANTS, PROFILE for faculty
|
||||
type FacultyExtendedView = FacultyView | 'STATUS_TRACKER' | 'USER_MANAGEMENT' | 'PARTICIPANTS' | 'PROFILE' | 'VERIFY' | 'TRACK_VENUE';
|
||||
|
||||
const Toast: React.FC<{ message: string; onClose: () => void }> = ({ message, onClose }) => {
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(onClose, 3000);
|
||||
return () => clearTimeout(timer);
|
||||
}, [onClose]);
|
||||
|
||||
return createPortal(
|
||||
<div className="fixed bottom-10 left-1/2 -translate-x-1/2 z-[10000] bg-emerald-600 text-white px-8 py-4 rounded-2xl shadow-2xl font-black text-xs uppercase tracking-widest flex items-center gap-3 animate-in slide-in-from-bottom-10">
|
||||
<i className="fas fa-check-circle"></i>
|
||||
{message}
|
||||
</div>,
|
||||
document.body
|
||||
);
|
||||
};
|
||||
|
||||
const FacultyDashboard: React.FC<FacultyDashboardProps> = ({
|
||||
onLogout,
|
||||
events,
|
||||
announcements,
|
||||
studentDocuments,
|
||||
onApproveCertificate,
|
||||
localRegistrations = [],
|
||||
onViewStudentHub,
|
||||
currentUserName,
|
||||
currentUserDept,
|
||||
currentUserEmail,
|
||||
onUpdateEvent,
|
||||
specialEvents = [],
|
||||
onUpdateSpecialEvent
|
||||
}) => {
|
||||
const [activeView, setActiveView] = useState<FacultyExtendedView>('OVERVIEW');
|
||||
const [isLoaded, setIsLoaded] = useState(false);
|
||||
const [toastMsg, setToastMsg] = useState<string | null>(null);
|
||||
const [processingIds, setProcessingIds] = useState<Set<string>>(new Set());
|
||||
const [removedIds, setRemovedIds] = useState<Set<string>>(new Set());
|
||||
const [pendingDomains, setPendingDomains] = useState<any[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
setIsLoaded(true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeView === 'VERIFY') {
|
||||
fetchPendingDomains();
|
||||
}
|
||||
}, [activeView]);
|
||||
|
||||
const fetchPendingDomains = async () => {
|
||||
const { data, error } = await supabase.from('domains').select('*').eq('status', 'PENDING');
|
||||
if (!error && data) {
|
||||
setPendingDomains(data);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCloseToast = useCallback(() => setToastMsg(null), []);
|
||||
|
||||
const menuItems: { id: FacultyExtendedView; label: string; icon: string }[] = [
|
||||
{ id: 'OVERVIEW', label: 'OVERVIEW', icon: 'fa-chart-pie' },
|
||||
{ id: 'STATUS_TRACKER', label: 'TRACKER', icon: 'fa-route' },
|
||||
{ id: 'VERIFY', label: 'VERIFY', icon: 'fa-shield-check' },
|
||||
{ id: 'TRACK_VENUE', label: 'VENUES', icon: 'fa-map-marker-alt' },
|
||||
{ id: 'USER_MANAGEMENT', label: 'USERS', icon: 'fa-users-cog' },
|
||||
{ id: 'PARTICIPANTS', label: 'PARTICIPANTS', icon: 'fa-users' },
|
||||
{ id: 'NOTIFICATIONS', label: 'NOTICES', icon: 'fa-bell' },
|
||||
{ id: 'PROFILE', label: 'PROFILE', icon: 'fa-user-tie' },
|
||||
];
|
||||
|
||||
const handleToast = (msg: string) => setToastMsg(msg);
|
||||
|
||||
const renderView = () => {
|
||||
switch (activeView) {
|
||||
case 'OVERVIEW':
|
||||
return (
|
||||
<div className="animate-in fade-in zoom-in-95 duration-700 h-full flex flex-col items-center justify-center text-center py-20 relative z-10">
|
||||
<div className="bg-[#004a99]/10 border border-[#004a99]/20 px-6 py-2 rounded-full mb-8 backdrop-blur-md">
|
||||
<span className="text-[10px] font-black text-[#004a99] uppercase tracking-[0.3em]">Institutional Admin Portal</span>
|
||||
</div>
|
||||
<h1 className="text-7xl font-black text-slate-900 uppercase tracking-tighter mb-6 leading-none">
|
||||
ADMIN <span className="text-transparent bg-clip-text bg-gradient-to-r from-[#004a99] to-orange-500">HUB</span>
|
||||
</h1>
|
||||
<p className="text-slate-600 text-xl font-medium max-w-2xl mb-12 uppercase tracking-widest leading-relaxed">
|
||||
Orchestrate Academic Excellence <br />
|
||||
<span className="text-sm font-bold text-slate-400">Review student progress, track events, and oversee departmental activities.</span>
|
||||
</p>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6 w-full max-w-4xl px-6">
|
||||
<button
|
||||
onClick={() => setActiveView('STATUS_TRACKER')}
|
||||
className="px-10 py-6 bg-white border border-slate-200 text-[#004a99] rounded-3xl font-black uppercase text-xs tracking-[0.2em] hover:bg-[#004a99] hover:text-white transition-all shadow-xl shadow-slate-200 hover:-translate-y-1"
|
||||
>
|
||||
Track Student Progress <i className="fas fa-route ml-2"></i>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveView('USER_MANAGEMENT')}
|
||||
className="px-10 py-6 bg-slate-900 text-white rounded-3xl font-black uppercase text-xs tracking-[0.2em] hover:bg-black transition-all shadow-xl shadow-slate-900/20 hover:-translate-y-1"
|
||||
>
|
||||
Manage Users <i className="fas fa-users-cog ml-2"></i>
|
||||
</button>
|
||||
{/* Coordinator Hub navigation explicitly removed for Admin Hub pure view */}
|
||||
{onViewStudentHub && (
|
||||
<button
|
||||
onClick={onViewStudentHub}
|
||||
className="px-10 py-6 bg-emerald-600 text-white rounded-3xl font-black uppercase text-xs tracking-[0.2em] hover:bg-emerald-700 transition-all shadow-xl shadow-emerald-600/20 hover:-translate-y-1"
|
||||
>
|
||||
Student Hub <i className="fas fa-graduation-cap ml-2"></i>
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setActiveView('VERIFY')}
|
||||
className="px-10 py-6 bg-amber-500 text-white rounded-3xl font-black uppercase text-xs tracking-[0.2em] hover:bg-amber-600 transition-all shadow-xl shadow-amber-600/20 hover:-translate-y-1"
|
||||
>
|
||||
Event Verification <i className="fas fa-shield-check ml-2"></i>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveView('TRACK_VENUE')}
|
||||
className="px-10 py-6 bg-indigo-600 text-white rounded-3xl font-black uppercase text-xs tracking-[0.2em] hover:bg-indigo-700 transition-all shadow-xl shadow-indigo-600/20 hover:-translate-y-1"
|
||||
>
|
||||
Track Venue <i className="fas fa-map-marker-alt ml-2"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
case 'VERIFY': {
|
||||
const pendingEvents = events.filter(e => e.verificationStatus === 'PENDING_ADMIN' && !removedIds.has(e.id));
|
||||
const pendingSpecialEvents = specialEvents.filter(se => (se.verificationStatus === 'PENDING' || se.verificationStatus === 'PENDING_ADMIN') && !removedIds.has(se.id));
|
||||
|
||||
const handleVerifyAction = async (item: any, status: 'APPROVED' | 'REJECTED', isSpecial: boolean) => {
|
||||
if (processingIds.has(item.id)) return;
|
||||
|
||||
setProcessingIds(prev => new Set(prev).add(item.id));
|
||||
|
||||
try {
|
||||
if (isSpecial && onUpdateSpecialEvent) {
|
||||
await onUpdateSpecialEvent({ ...item, verificationStatus: status });
|
||||
} else {
|
||||
await onUpdateEvent({ ...item, verificationStatus: status });
|
||||
}
|
||||
// Optimistic removal
|
||||
setRemovedIds(prev => new Set(prev).add(item.id));
|
||||
handleToast(`"${item.title}" ${status === 'APPROVED' ? 'Approved' : 'Rejected'} successfully!`);
|
||||
} catch (err) {
|
||||
handleToast(`Failed to update "${item.title}". Please try again.`);
|
||||
} finally {
|
||||
setProcessingIds(prev => {
|
||||
const next = new Set(prev);
|
||||
next.delete(item.id);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="animate-in fade-in slide-in-from-bottom-10 duration-500 h-full flex flex-col">
|
||||
<div className="mb-10 flex items-end justify-between">
|
||||
<div>
|
||||
<h2 className="text-4xl font-black text-slate-900 tracking-tighter uppercase mb-2">Verification Queue</h2>
|
||||
<p className="text-amber-600 font-bold text-[10px] tracking-[0.3em] uppercase flex items-center gap-2">
|
||||
<span className="w-2 h-2 bg-amber-500 rounded-full animate-pulse"></span>
|
||||
{(pendingEvents.length + pendingSpecialEvents.length + pendingDomains.filter((d: any) => !removedIds.has(d.id)).length)} Submissions Awaiting Audit
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 pb-20">
|
||||
{/* Standard Events */}
|
||||
{pendingEvents.map((event) => (
|
||||
<div key={event.id} className="bg-white border border-slate-200 rounded-[2.5rem] overflow-hidden group shadow-sm flex flex-col">
|
||||
<div className="h-44 relative overflow-hidden bg-slate-100">
|
||||
<img src={event.image} alt={event.title} className="w-full h-full object-cover" />
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent"></div>
|
||||
<div className="absolute top-4 right-4 bg-white/90 backdrop-blur-md px-4 py-1.5 rounded-full text-[9px] font-black uppercase tracking-widest text-[#004a99]">Normal Event</div>
|
||||
<div className="absolute bottom-4 left-4">
|
||||
<span className={`px-3 py-1 rounded-full text-[8px] font-black uppercase tracking-widest ${event.pricingType === 'PAID' ? 'bg-rose-500 text-white' : 'bg-emerald-500 text-white'}`}>{event.pricingType}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-6 flex-1 flex flex-col gap-0">
|
||||
<h3 className="text-lg font-black text-slate-900 uppercase tracking-tight mb-1">{event.title}</h3>
|
||||
<p className="text-[9px] text-[#004a99] font-black uppercase tracking-widest mb-4">Normal Event · {event.category}</p>
|
||||
|
||||
{/* Full Details Line-by-Line */}
|
||||
<div className="space-y-2 mb-5 text-[10px] font-bold text-slate-600 uppercase tracking-wide">
|
||||
<div className="flex items-start gap-2"><i className="fas fa-user-tie text-[#004a99] w-4 mt-0.5"></i><span><span className="text-slate-400">Coordinator:</span> {event.coordinator || '—'}</span></div>
|
||||
<div className="flex items-start gap-2"><i className="fas fa-building text-[#004a99] w-4 mt-0.5"></i><span><span className="text-slate-400">Dept:</span> {event.conducting_dept || '—'}</span></div>
|
||||
<div className="flex items-start gap-2"><i className="fas fa-users text-[#004a99] w-4 mt-0.5"></i><span><span className="text-slate-400">Club/Entity:</span> {event.club || '—'}</span></div>
|
||||
<div className="flex items-start gap-2"><i className="far fa-calendar text-orange-500 w-4 mt-0.5"></i><span><span className="text-slate-400">Date:</span> {event.date || '—'}</span></div>
|
||||
<div className="flex items-start gap-2"><i className="fas fa-clock text-orange-500 w-4 mt-0.5"></i><span><span className="text-slate-400">Time:</span> {event.schedule?.[0]?.start_time || '—'}</span></div>
|
||||
<div className="flex items-start gap-2"><i className="fas fa-location-dot text-orange-500 w-4 mt-0.5"></i><span><span className="text-slate-400">Venue:</span> {event.location || '—'}</span></div>
|
||||
<div className="flex items-start gap-2"><i className="fas fa-calendar-week text-slate-400 w-4 mt-0.5"></i><span><span className="text-slate-400">Duration:</span> {event.durationDays || 1} Day{(event.durationDays || 1) > 1 ? 's' : ''}</span></div>
|
||||
<div className="flex items-start gap-2"><i className="fas fa-layer-group text-slate-400 w-4 mt-0.5"></i><span><span className="text-slate-400">Domain:</span> {event.domain || '—'}</span></div>
|
||||
<div className="flex items-start gap-2"><i className="fas fa-users text-slate-400 w-4 mt-0.5"></i><span><span className="text-slate-400">Capacity:</span> {event.maxParticipants ? `${event.maxParticipants} seats` : 'Unlimited'}</span></div>
|
||||
<div className="flex items-start gap-2"><i className="fas fa-globe text-slate-400 w-4 mt-0.5"></i><span><span className="text-slate-400">Access:</span> {event.participantType || 'INTERNAL'}</span></div>
|
||||
{event.isTeamEvent && <div className="flex items-start gap-2"><i className="fas fa-people-arrows text-slate-400 w-4 mt-0.5"></i><span><span className="text-slate-400">Team Size:</span> {event.teamSizeLimit || '—'} ({event.teamComposition})</span></div>}
|
||||
{event.registrationDeadline && <div className="flex items-start gap-2"><i className="fas fa-hourglass-end text-rose-400 w-4 mt-0.5"></i><span><span className="text-slate-400">Deadline:</span> {new Date(event.registrationDeadline).toLocaleString()}</span></div>}
|
||||
{event.event_summary && (
|
||||
<div className="flex items-start gap-2 pt-1">
|
||||
<i className="fas fa-align-left text-slate-400 w-4 mt-0.5"></i>
|
||||
<span className="italic text-slate-500 font-medium normal-case tracking-normal line-clamp-3">{event.event_summary}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Financial Projection Section */}
|
||||
<div className="bg-slate-50 border border-slate-100 rounded-2xl p-4 mb-5 space-y-2">
|
||||
<p className="text-[8px] font-black text-slate-400 uppercase tracking-widest mb-2">Financial Projections</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[9px] font-bold text-slate-500 uppercase">Refreshment</span>
|
||||
<span className="text-[10px] font-black text-slate-800">₹{event.refreshment_expense || 0}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[9px] font-bold text-slate-500 uppercase">Transportation</span>
|
||||
<span className="text-[10px] font-black text-slate-800">₹{event.transportation_expense || 0}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[9px] font-bold text-slate-500 uppercase">Session Coverage</span>
|
||||
<span className="text-[10px] font-black text-slate-800">₹{(event as any).session_coverage_fee || 0}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between pt-2 border-t border-slate-200">
|
||||
<span className="text-[9px] font-black text-emerald-600 uppercase">Total Projected</span>
|
||||
<span className="text-xs font-black text-emerald-700">₹{event.total_expense || 0}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 mt-auto">
|
||||
<button
|
||||
onClick={() => generateEventDetailsPDF(event)}
|
||||
className="w-12 py-4 bg-blue-50 text-blue-600 rounded-2xl flex items-center justify-center hover:bg-blue-100 transition-all active:scale-95 shadow-sm border border-blue-100 flex-shrink-0"
|
||||
title="Download Event Report"
|
||||
>
|
||||
<i className="fas fa-file-pdf"></i>
|
||||
</button>
|
||||
<button
|
||||
disabled={processingIds.has(event.id)}
|
||||
onClick={() => handleVerifyAction(event, 'APPROVED', false)}
|
||||
className="flex-1 py-4 bg-emerald-600 text-white rounded-2xl text-[10px] font-black uppercase tracking-widest hover:bg-emerald-700 transition-all active:scale-95 shadow-lg shadow-emerald-600/20 disabled:opacity-50"
|
||||
>
|
||||
{processingIds.has(event.id) ? <i className="fas fa-circle-notch fa-spin"></i> : <><i className="fas fa-check mr-1"></i> Approve</>}
|
||||
</button>
|
||||
<button
|
||||
disabled={processingIds.has(event.id)}
|
||||
onClick={() => handleVerifyAction(event, 'REJECTED', false)}
|
||||
className="flex-1 py-4 bg-rose-50 text-rose-600 border border-rose-100 rounded-2xl text-[10px] font-black uppercase tracking-widest hover:bg-rose-600 hover:text-white transition-all active:scale-95 disabled:opacity-50"
|
||||
>
|
||||
{processingIds.has(event.id) ? <i className="fas fa-circle-notch fa-spin"></i> : <><i className="fas fa-times mr-1"></i> Reject</>}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Special Events */}
|
||||
{pendingSpecialEvents.map((event) => (
|
||||
<div key={event.id} className="bg-slate-900 text-white rounded-[2.5rem] overflow-hidden group shadow-xl flex flex-col border border-white/5">
|
||||
<div className="h-48 relative overflow-hidden bg-slate-800 flex items-center justify-center">
|
||||
<i className="fas fa-star text-4xl text-orange-500 opacity-20"></i>
|
||||
<div className="absolute top-4 right-4 bg-orange-500 text-white px-4 py-1.5 rounded-full text-[9px] font-black uppercase tracking-widest">Special Event</div>
|
||||
</div>
|
||||
<div className="p-8 flex-1 flex flex-col">
|
||||
<h3 className="text-xl font-black text-white uppercase tracking-tight mb-2 line-clamp-1">{event.title}</h3>
|
||||
<p className="text-[10px] text-slate-400 font-bold uppercase tracking-widest mb-6">External Link Portal</p>
|
||||
|
||||
<div className="space-y-3 mb-8 flex-1">
|
||||
<div className="flex items-center gap-3 text-slate-500 font-bold text-[9px] uppercase tracking-widest">
|
||||
<i className="fas fa-link text-orange-500"></i>
|
||||
<span className="truncate">{event.link}</span>
|
||||
</div>
|
||||
<p className="text-[10px] text-slate-300 italic line-clamp-2">{event.description}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
disabled={processingIds.has(event.id)}
|
||||
onClick={() => handleVerifyAction(event, 'APPROVED', true)}
|
||||
className="flex-1 py-4 bg-orange-500 text-white rounded-2xl text-[10px] font-black uppercase tracking-widest hover:bg-orange-600 transition-all active:scale-95 shadow-lg shadow-orange-500/20 disabled:opacity-50 group/btn"
|
||||
>
|
||||
{processingIds.has(event.id) ? <i className="fas fa-circle-notch fa-spin"></i> : "Approve"}
|
||||
</button>
|
||||
<button
|
||||
disabled={processingIds.has(event.id)}
|
||||
onClick={() => handleVerifyAction(event, 'REJECTED', true)}
|
||||
className="flex-1 py-4 bg-white/5 text-white border border-white/10 rounded-2xl text-[10px] font-black uppercase tracking-widest hover:bg-white/10 transition-all active:scale-95 disabled:opacity-50"
|
||||
>
|
||||
{processingIds.has(event.id) ? <i className="fas fa-circle-notch fa-spin"></i> : "Reject"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Pending Domains */}
|
||||
{pendingDomains.filter((d: any) => !removedIds.has(d.id)).map((domain) => (
|
||||
<div key={domain.id} className="bg-white border border-emerald-200 rounded-[2.5rem] overflow-hidden group shadow-sm flex flex-col">
|
||||
<div className="h-48 relative overflow-hidden bg-slate-100 flex items-center justify-center italic">
|
||||
<img src={domain.image} alt={domain.name} className="w-full h-full object-cover" />
|
||||
<div className="absolute top-4 right-4 bg-emerald-500 text-white px-4 py-1.5 rounded-full text-[9px] font-black uppercase tracking-widest">New Domain</div>
|
||||
</div>
|
||||
<div className="p-8 flex-1 flex flex-col">
|
||||
<h3 className="text-xl font-black text-slate-900 uppercase tracking-tight mb-2 line-clamp-1">{domain.name}</h3>
|
||||
<p className="text-[10px] text-emerald-600 font-bold uppercase tracking-widest mb-6">Category: {domain.category}</p>
|
||||
|
||||
<div className="space-y-3 mb-8 flex-1">
|
||||
<p className="text-[10px] text-slate-500 font-bold line-clamp-3">{domain.description || 'No description provided'}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
disabled={processingIds.has(domain.id)}
|
||||
onClick={async () => {
|
||||
if (processingIds.has(domain.id)) return;
|
||||
setProcessingIds(prev => new Set(prev).add(domain.id));
|
||||
try {
|
||||
await supabase.from('domains').update({ status: 'APPROVED' }).eq('id', domain.id);
|
||||
setRemovedIds(prev => new Set(prev).add(domain.id));
|
||||
handleToast(`Domain "${domain.name}" Approved successfully!`);
|
||||
} catch (err) {
|
||||
handleToast(`Failed to update "${domain.name}". Please try again.`);
|
||||
} finally {
|
||||
setProcessingIds(prev => { const next = new Set(prev); next.delete(domain.id); return next; });
|
||||
}
|
||||
}}
|
||||
className="flex-1 py-4 bg-emerald-600 text-white rounded-2xl text-[10px] font-black uppercase tracking-widest hover:bg-emerald-700 transition-all active:scale-95 shadow-lg shadow-emerald-600/20 disabled:opacity-50 group/btn"
|
||||
>
|
||||
{processingIds.has(domain.id) ? <i className="fas fa-circle-notch fa-spin"></i> : "Approve"}
|
||||
</button>
|
||||
<button
|
||||
disabled={processingIds.has(domain.id)}
|
||||
onClick={async () => {
|
||||
if (processingIds.has(domain.id)) return;
|
||||
setProcessingIds(prev => new Set(prev).add(domain.id));
|
||||
try {
|
||||
await supabase.from('domains').update({ status: 'REJECTED' }).eq('id', domain.id);
|
||||
setRemovedIds(prev => new Set(prev).add(domain.id));
|
||||
handleToast(`Domain "${domain.name}" Rejected!`);
|
||||
} catch (err) {
|
||||
handleToast(`Failed to update "${domain.name}". Please try again.`);
|
||||
} finally {
|
||||
setProcessingIds(prev => { const next = new Set(prev); next.delete(domain.id); return next; });
|
||||
}
|
||||
}}
|
||||
className="flex-1 py-4 bg-rose-50 text-rose-600 border border-rose-100 rounded-2xl text-[10px] font-black uppercase tracking-widest hover:bg-rose-600 hover:text-white transition-all active:scale-95 disabled:opacity-50"
|
||||
>
|
||||
{processingIds.has(domain.id) ? <i className="fas fa-circle-notch fa-spin"></i> : "Reject"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{pendingEvents.length === 0 && pendingSpecialEvents.length === 0 && pendingDomains.filter((d: any) => !removedIds.has(d.id)).length === 0 && (
|
||||
<div className="col-span-full py-40 flex flex-col items-center justify-center bg-white/50 border border-dashed border-slate-200 rounded-[3rem] text-center px-10">
|
||||
<div className="w-20 h-20 bg-emerald-50 rounded-full flex items-center justify-center mb-6">
|
||||
<i className="fas fa-check-double text-2xl text-emerald-500"></i>
|
||||
</div>
|
||||
<h4 className="text-slate-900 font-black uppercase tracking-[0.3em] text-xs mb-3">Audit Complete</h4>
|
||||
<p className="text-slate-500 font-bold uppercase tracking-[0.2em] text-[9px] max-w-sm">No submissions remaining in the verification pipeline.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ── Verified & Scheduled Records ── */}
|
||||
{(() => {
|
||||
const verifiedEvents = events.filter(e => e.verificationStatus === 'APPROVED');
|
||||
const verifiedSpecial = specialEvents.filter(se => se.verificationStatus === 'APPROVED');
|
||||
const totalVerified = verifiedEvents.length + verifiedSpecial.length;
|
||||
return totalVerified > 0 ? (
|
||||
<div className="mt-16">
|
||||
<div className="flex items-center gap-4 mb-8">
|
||||
<div className="w-8 h-8 bg-emerald-100 rounded-xl flex items-center justify-center">
|
||||
<i className="fas fa-clipboard-check text-emerald-600 text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xl font-black text-slate-900 tracking-tighter uppercase">Verified & Scheduled Records</h3>
|
||||
<p className="text-[10px] font-bold text-emerald-600 uppercase tracking-widest">{totalVerified} approved event{totalVerified !== 1 ? 's' : ''} on record</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white border border-slate-200 rounded-[2rem] overflow-hidden shadow-sm">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="bg-slate-50 border-b border-slate-100">
|
||||
<th className="text-left px-6 py-4 text-[9px] font-black text-slate-400 uppercase tracking-[0.25em]">#</th>
|
||||
<th className="text-left px-6 py-4 text-[9px] font-black text-slate-400 uppercase tracking-[0.25em]">Event</th>
|
||||
<th className="text-left px-6 py-4 text-[9px] font-black text-slate-400 uppercase tracking-[0.25em]">Type</th>
|
||||
<th className="text-left px-6 py-4 text-[9px] font-black text-slate-400 uppercase tracking-[0.25em]">Coordinator</th>
|
||||
<th className="text-left px-6 py-4 text-[9px] font-black text-slate-400 uppercase tracking-[0.25em]">Date</th>
|
||||
<th className="text-left px-6 py-4 text-[9px] font-black text-slate-400 uppercase tracking-[0.25em]">Venue</th>
|
||||
<th className="text-left px-6 py-4 text-[9px] font-black text-slate-400 uppercase tracking-[0.25em]">Category</th>
|
||||
<th className="text-left px-6 py-4 text-[9px] font-black text-slate-400 uppercase tracking-[0.25em]">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-50">
|
||||
{verifiedEvents.map((ev, idx) => (
|
||||
<tr key={ev.id} className="hover:bg-slate-50/70 transition-colors group">
|
||||
<td className="px-6 py-4 text-[10px] font-black text-slate-300">{idx + 1}</td>
|
||||
<td className="px-6 py-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-xl overflow-hidden bg-slate-100 flex-shrink-0">
|
||||
{ev.image ? (
|
||||
<img src={ev.image} alt={ev.title} className="w-full h-full object-cover" />
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center"><i className="fas fa-calendar text-slate-300 text-xs"></i></div>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-[11px] font-black text-slate-900 uppercase tracking-tight line-clamp-1 max-w-[160px]">{ev.title}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4"><span className="px-3 py-1 bg-blue-50 text-[#004a99] text-[8px] font-black uppercase tracking-widest rounded-full border border-blue-100">Normal</span></td>
|
||||
<td className="px-6 py-4 text-[10px] font-bold text-slate-600 uppercase">{ev.coordinator || '—'}</td>
|
||||
<td className="px-6 py-4 text-[10px] font-bold text-slate-500 uppercase whitespace-nowrap">{ev.date || '—'}</td>
|
||||
<td className="px-6 py-4 text-[10px] font-bold text-slate-500 uppercase line-clamp-1 max-w-[120px]">{ev.location || '—'}</td>
|
||||
<td className="px-6 py-4">
|
||||
<span className="px-3 py-1 bg-slate-50 text-slate-600 text-[8px] font-black uppercase tracking-widest rounded-full border border-slate-100">{ev.category || 'General'}</span>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-2 h-2 rounded-full bg-emerald-500 shadow-[0_0_6px_rgba(16,185,129,0.6)]"></span>
|
||||
<span className="text-[9px] font-black text-emerald-600 uppercase tracking-widest">{ev.status || 'Scheduled'}</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{verifiedSpecial.map((ev, idx) => (
|
||||
<tr key={ev.id} className="hover:bg-orange-50/30 transition-colors group">
|
||||
<td className="px-6 py-4 text-[10px] font-black text-slate-300">{verifiedEvents.length + idx + 1}</td>
|
||||
<td className="px-6 py-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-xl bg-orange-100 flex items-center justify-center flex-shrink-0">
|
||||
<i className="fas fa-star text-orange-500 text-xs"></i>
|
||||
</div>
|
||||
<span className="text-[11px] font-black text-slate-900 uppercase tracking-tight line-clamp-1 max-w-[160px]">{ev.title}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4"><span className="px-3 py-1 bg-orange-50 text-orange-600 text-[8px] font-black uppercase tracking-widest rounded-full border border-orange-100">Special</span></td>
|
||||
<td className="px-6 py-4 text-[10px] font-bold text-slate-600 uppercase">{ev.coordinator || '—'}</td>
|
||||
<td className="px-6 py-4 text-[10px] font-bold text-slate-500 uppercase whitespace-nowrap">{ev.date || '—'}</td>
|
||||
<td className="px-6 py-4">
|
||||
<a href={ev.link} target="_blank" rel="noopener noreferrer" className="text-[10px] font-bold text-[#004a99] hover:underline truncate max-w-[100px] block">{ev.link || '—'}</a>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<span className="px-3 py-1 bg-slate-50 text-slate-600 text-[8px] font-black uppercase tracking-widest rounded-full border border-slate-100">Special</span>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-2 h-2 rounded-full bg-emerald-500 shadow-[0_0_6px_rgba(16,185,129,0.6)]"></span>
|
||||
<span className="text-[9px] font-black text-emerald-600 uppercase tracking-widest">Approved</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null;
|
||||
})()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
case 'STATUS_TRACKER':
|
||||
return (
|
||||
<AdminStatusTrackerView
|
||||
events={events}
|
||||
onShowToast={handleToast}
|
||||
studentDocuments={studentDocuments}
|
||||
onApproveCertificate={onApproveCertificate}
|
||||
localRegistrations={localRegistrations}
|
||||
isAdmin={true}
|
||||
/>
|
||||
);
|
||||
case 'USER_MANAGEMENT':
|
||||
return <UserManagementView />;
|
||||
case 'PARTICIPANTS':
|
||||
return <FacultyParticipantsView events={events} localRegistrations={localRegistrations} />;
|
||||
case 'NOTIFICATIONS':
|
||||
return <FacultyNotifications />;
|
||||
case 'PROFILE':
|
||||
return <FacultyProfileView onLogout={onLogout} name={currentUserName} dept={currentUserDept} email={currentUserEmail} />;
|
||||
case 'TRACK_VENUE': {
|
||||
const TrackVenueView = () => {
|
||||
const [selectedVenue, setSelectedVenue] = React.useState<string | null>(null);
|
||||
const DAY_NAMES = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
||||
const MONTH_NAMES = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
|
||||
const today = new Date();
|
||||
|
||||
const venues = Array.from(new Set(events.filter(e => e.location).map(e => e.location))).sort();
|
||||
const venueEvents = selectedVenue ? events.filter(e => e.location === selectedVenue) : [];
|
||||
|
||||
const [calYear, setCalYear] = React.useState(() => {
|
||||
let first: Date | null = null;
|
||||
venueEvents.forEach(ev => {
|
||||
try { const d = new Date(ev.date); if (!isNaN(d.getTime()) && (first === null || d < first)) first = d; } catch { /* skip */ }
|
||||
});
|
||||
return (first ?? today).getFullYear();
|
||||
});
|
||||
const [calMonth, setCalMonth] = React.useState(() => {
|
||||
let first: Date | null = null;
|
||||
venueEvents.forEach(ev => {
|
||||
try { const d = new Date(ev.date); if (!isNaN(d.getTime()) && (first === null || d < first)) first = d; } catch { /* skip */ }
|
||||
});
|
||||
return (first ?? today).getMonth();
|
||||
});
|
||||
|
||||
const prevMonthFn = () => { if (calMonth === 0) { setCalMonth(11); setCalYear(y => y - 1); } else setCalMonth(m => m - 1); };
|
||||
const nextMonthFn = () => { if (calMonth === 11) { setCalMonth(0); setCalYear(y => y + 1); } else setCalMonth(m => m + 1); };
|
||||
const goToday = () => { setCalYear(today.getFullYear()); setCalMonth(today.getMonth()); };
|
||||
|
||||
// dateKey → events[]
|
||||
const eventsByDate: Record<string, typeof events> = {};
|
||||
venueEvents.forEach(ev => {
|
||||
try {
|
||||
const base = new Date(ev.date);
|
||||
if (isNaN(base.getTime())) return;
|
||||
const numDays = Math.max(ev.durationDays || 1, 1);
|
||||
for (let i = 0; i < numDays; i++) {
|
||||
const dd = new Date(base); dd.setDate(base.getDate() + i);
|
||||
const key = dd.getFullYear() + '-' + String(dd.getMonth() + 1).padStart(2, '0') + '-' + String(dd.getDate()).padStart(2, '0');
|
||||
eventsByDate[key] = [...(eventsByDate[key] || []), ev];
|
||||
}
|
||||
} catch { /* skip */ }
|
||||
});
|
||||
|
||||
// 42-cell calendar grid
|
||||
const firstDOW = new Date(calYear, calMonth, 1).getDay();
|
||||
const daysInMonth = new Date(calYear, calMonth + 1, 0).getDate();
|
||||
const prevMonthDays = new Date(calYear, calMonth, 0).getDate();
|
||||
type Cell = { day: number; kind: 'prev' | 'cur' | 'next'; key: string };
|
||||
const cells: Cell[] = [];
|
||||
for (let i = 0; i < firstDOW; i++) {
|
||||
const d = prevMonthDays - firstDOW + 1 + i;
|
||||
const m = calMonth === 0 ? 11 : calMonth - 1;
|
||||
const y = calMonth === 0 ? calYear - 1 : calYear;
|
||||
cells.push({ day: d, kind: 'prev', key: y + '-' + String(m + 1).padStart(2, '0') + '-' + String(d).padStart(2, '0') });
|
||||
}
|
||||
for (let d = 1; d <= daysInMonth; d++) {
|
||||
cells.push({ day: d, kind: 'cur', key: calYear + '-' + String(calMonth + 1).padStart(2, '0') + '-' + String(d).padStart(2, '0') });
|
||||
}
|
||||
const rest = 42 - cells.length;
|
||||
for (let d = 1; d <= rest; d++) {
|
||||
const m = calMonth === 11 ? 0 : calMonth + 1;
|
||||
const y = calMonth === 11 ? calYear + 1 : calYear;
|
||||
cells.push({ day: d, kind: 'next', key: y + '-' + String(m + 1).padStart(2, '0') + '-' + String(d).padStart(2, '0') });
|
||||
}
|
||||
|
||||
// Extract batch info from an event
|
||||
const getBatches = (ev: Event): { label: string; time: string }[] => {
|
||||
const r: { label: string; time: string }[] = [];
|
||||
if (ev.schedule && ev.schedule.length > 0) {
|
||||
// Group by day_idx
|
||||
const dayMap = new Map<number, typeof ev.schedule>();
|
||||
ev.schedule.forEach(s => {
|
||||
if (!dayMap.has(s.day_idx)) dayMap.set(s.day_idx, []);
|
||||
dayMap.get(s.day_idx)!.push(s);
|
||||
});
|
||||
const numDays = dayMap.size;
|
||||
dayMap.forEach((slots, dayIdx) => {
|
||||
slots!.forEach(slot => {
|
||||
const label = numDays > 1 ? `Day ${dayIdx} · Batch ${slot.batch_idx}` : `Batch ${slot.batch_idx}`;
|
||||
r.push({ label, time: `${slot.start_time} → ${slot.end_time}` });
|
||||
});
|
||||
});
|
||||
}
|
||||
if (r.length === 0) r.push({ label: 'Session', time: '—' });
|
||||
return r;
|
||||
};
|
||||
|
||||
const chipBg = (s?: string) =>
|
||||
s === 'APPROVED' ? 'bg-emerald-100 border-emerald-300 text-emerald-900' :
|
||||
(s === 'PENDING' || s === 'PENDING_HOD' || s === 'PENDING_ADMIN') ? 'bg-amber-100 border-amber-300 text-amber-900' :
|
||||
'bg-rose-100 border-rose-300 text-rose-900';
|
||||
const dotCls = (s?: string) => s === 'APPROVED' ? 'bg-emerald-500' : (s === 'PENDING' || s === 'PENDING_HOD' || s === 'PENDING_ADMIN') ? 'bg-amber-400 animate-pulse' : 'bg-rose-500';
|
||||
const txtCls = (s?: string) => s === 'APPROVED' ? 'text-emerald-600' : (s === 'PENDING' || s === 'PENDING_HOD' || s === 'PENDING_ADMIN') ? 'text-amber-600' : 'text-rose-600';
|
||||
|
||||
return (
|
||||
<div className="animate-in fade-in slide-in-from-bottom-10 duration-500 pb-20">
|
||||
{/* Header */}
|
||||
<div className="mb-8 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-4xl font-black text-slate-900 tracking-tighter uppercase mb-1">Track Venue</h2>
|
||||
<p className="text-indigo-600 font-bold text-[10px] tracking-[0.3em] uppercase flex items-center gap-2">
|
||||
<span className="w-2 h-2 bg-indigo-500 rounded-full"></span>
|
||||
{selectedVenue ? `Monthly Schedule — ${selectedVenue}` : 'Select a venue to view its full calendar'}
|
||||
</p>
|
||||
</div>
|
||||
{selectedVenue && (
|
||||
<button onClick={() => setSelectedVenue(null)}
|
||||
className="flex items-center gap-2 text-[10px] font-black text-slate-500 hover:text-indigo-600 uppercase tracking-widest border border-slate-200 bg-white px-5 py-2.5 rounded-xl hover:border-indigo-300 hover:shadow-md transition-all">
|
||||
<i className="fas fa-arrow-left text-[8px]"></i> All Venues
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!selectedVenue ? (
|
||||
venues.length === 0 ? (
|
||||
<div className="py-40 flex flex-col items-center justify-center bg-white/50 border border-dashed border-slate-200 rounded-[3rem] text-center px-10">
|
||||
<div className="w-20 h-20 bg-indigo-50 rounded-full flex items-center justify-center mb-6">
|
||||
<i className="fas fa-map-marker-alt text-2xl text-indigo-400"></i>
|
||||
</div>
|
||||
<h4 className="text-slate-900 font-black uppercase tracking-[0.3em] text-xs mb-3">No Venues Found</h4>
|
||||
<p className="text-slate-500 font-bold uppercase tracking-[0.2em] text-[9px] max-w-sm">No events with venue data added yet.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
|
||||
{venues.map(venue => {
|
||||
const va = events.filter(e => e.location === venue);
|
||||
const ap = va.filter(e => e.verificationStatus === 'APPROVED').length;
|
||||
const pe = va.filter(e => e.verificationStatus === 'PENDING').length;
|
||||
return (
|
||||
<button key={venue} onClick={() => setSelectedVenue(venue)}
|
||||
className="group bg-white border border-slate-200 rounded-[2rem] p-8 text-left hover:border-indigo-400 hover:shadow-xl hover:shadow-indigo-500/10 transition-all duration-300 hover:-translate-y-1 active:scale-95">
|
||||
<div className="w-12 h-12 bg-indigo-50 rounded-2xl flex items-center justify-center mb-5 group-hover:bg-indigo-100 transition-colors">
|
||||
<i className="fas fa-map-marker-alt text-indigo-500"></i>
|
||||
</div>
|
||||
<h3 className="text-lg font-black text-slate-900 uppercase tracking-tight mb-1 group-hover:text-indigo-600">{venue}</h3>
|
||||
<p className="text-[9px] font-bold text-slate-400 uppercase tracking-widest mb-4">{va.length} Event{va.length !== 1 ? 's' : ''} Total</p>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{ap > 0 && <span className="px-2 py-0.5 bg-emerald-50 border border-emerald-200 text-emerald-700 text-[8px] font-black rounded-full uppercase">{ap} Approved</span>}
|
||||
{pe > 0 && <span className="px-2 py-0.5 bg-amber-50 border border-amber-200 text-amber-700 text-[8px] font-black rounded-full uppercase">{pe} Pending</span>}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
{/* Venue info + legend */}
|
||||
<div className="bg-white border border-slate-200 rounded-[2rem] p-5 mb-6 flex flex-wrap items-center gap-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-11 h-11 bg-indigo-100 rounded-2xl flex items-center justify-center">
|
||||
<i className="fas fa-map-marker-alt text-indigo-600 text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-black text-slate-900 uppercase tracking-tight leading-none">{selectedVenue}</h3>
|
||||
<p className="text-[9px] font-bold text-slate-400 uppercase tracking-widest mt-0.5">{venueEvents.length} event{venueEvents.length !== 1 ? 's' : ''} at this venue</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-5 ml-auto flex-wrap">
|
||||
<div className="flex items-center gap-1.5"><span className="w-2.5 h-2.5 rounded-full bg-emerald-500"></span><span className="text-[9px] font-black uppercase tracking-widest text-slate-500">Approved</span></div>
|
||||
<div className="flex items-center gap-1.5"><span className="w-2.5 h-2.5 rounded-full bg-amber-400 animate-pulse"></span><span className="text-[9px] font-black uppercase tracking-widest text-slate-500">Pending</span></div>
|
||||
<div className="flex items-center gap-1.5"><span className="w-2.5 h-2.5 rounded-full bg-rose-400"></span><span className="text-[9px] font-black uppercase tracking-widest text-slate-500">Rejected</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Calendar Card */}
|
||||
<div className="bg-white border border-slate-200 rounded-[2rem] overflow-hidden shadow-sm mb-8">
|
||||
{/* Month nav header */}
|
||||
<div className="flex items-center justify-between px-8 py-5 bg-gradient-to-r from-indigo-600 to-indigo-500">
|
||||
<button onClick={prevMonthFn}
|
||||
className="w-9 h-9 rounded-xl bg-white/15 border border-white/20 flex items-center justify-center hover:bg-white/25 transition-all">
|
||||
<i className="fas fa-chevron-left text-[10px] text-white"></i>
|
||||
</button>
|
||||
<div className="text-center select-none">
|
||||
<h3 className="text-3xl font-black text-white uppercase tracking-tight leading-none">{MONTH_NAMES[calMonth]}</h3>
|
||||
<p className="text-[14px] font-bold text-white/70 uppercase tracking-[0.5em] mt-1">{calYear}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button onClick={goToday}
|
||||
className="px-4 py-2 rounded-xl bg-white text-indigo-600 text-[9px] font-black uppercase tracking-widest hover:bg-indigo-50 transition-all shadow-sm">
|
||||
Today
|
||||
</button>
|
||||
<button onClick={nextMonthFn}
|
||||
className="w-9 h-9 rounded-xl bg-white/15 border border-white/20 flex items-center justify-center hover:bg-white/25 transition-all">
|
||||
<i className="fas fa-chevron-right text-[10px] text-white"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Day-of-week header row */}
|
||||
<div className="grid grid-cols-7 border-b border-slate-100 bg-slate-50">
|
||||
{['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'].map(d => (
|
||||
<div key={d} className="py-3 text-center border-r border-slate-100 last:border-r-0">
|
||||
<span className="hidden lg:block text-[9px] font-black uppercase tracking-[0.15em] text-slate-400">{d}</span>
|
||||
<span className="lg:hidden text-[9px] font-black uppercase tracking-[0.15em] text-slate-400">{d.slice(0, 3)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 42-cell grid */}
|
||||
<div className="grid grid-cols-7" style={{ gridAutoRows: 'minmax(115px, auto)' }}>
|
||||
{cells.map((cell, idx) => {
|
||||
const isCur = cell.kind === 'cur';
|
||||
const cellEvs = isCur ? (eventsByDate[cell.key] || []) : [];
|
||||
const hasEvs = cellEvs.length > 0;
|
||||
const isTod = isCur && cell.day === today.getDate() && calMonth === today.getMonth() && calYear === today.getFullYear();
|
||||
return (
|
||||
<div key={idx} className={[
|
||||
'border-b border-r border-slate-100 flex flex-col',
|
||||
!isCur ? 'bg-slate-50/70' : hasEvs ? 'bg-indigo-50/25' : 'bg-white hover:bg-slate-50/40'
|
||||
].join(' ')}>
|
||||
{/* Date number */}
|
||||
<div className="flex items-center justify-between px-2 pt-2 pb-1">
|
||||
<span className={[
|
||||
'w-7 h-7 flex items-center justify-center rounded-full text-[11px] font-black select-none transition-colors',
|
||||
isTod ? 'bg-indigo-600 text-white shadow-md shadow-indigo-400/40' :
|
||||
!isCur ? 'text-slate-300' :
|
||||
hasEvs ? 'text-indigo-700' :
|
||||
'text-slate-500'
|
||||
].join(' ')}>
|
||||
{cell.day}
|
||||
</span>
|
||||
{hasEvs && <span className="text-[7px] font-black text-indigo-400 uppercase">{cellEvs.length} ev</span>}
|
||||
</div>
|
||||
{/* Event chips */}
|
||||
<div className="px-1.5 pb-1.5 space-y-1 overflow-hidden flex-1 min-w-0">
|
||||
{cellEvs.map((ev, ei) => {
|
||||
const batches = getBatches(ev);
|
||||
return (
|
||||
<div key={ev.id + String(ei)} className={`rounded-lg border px-2 pt-1 pb-1.5 min-w-0 ${chipBg(ev.verificationStatus)}`}>
|
||||
<p className="text-[8px] font-black uppercase tracking-wide leading-tight truncate">{ev.title}</p>
|
||||
{batches.map((b, bi) => (
|
||||
<div key={bi} className="flex items-center gap-1 mt-0.5 flex-wrap">
|
||||
<span className="text-[7px] font-black opacity-80 uppercase bg-white/40 px-1.5 py-0.5 rounded-full leading-none whitespace-nowrap">{b.label}</span>
|
||||
<span className="text-[7px] font-bold opacity-70 leading-none whitespace-nowrap">{b.time}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Detail table */}
|
||||
{venueEvents.length > 0 && (
|
||||
<div className="bg-white border border-slate-200 rounded-[2rem] overflow-hidden shadow-sm">
|
||||
<div className="px-8 py-5 border-b border-slate-100 flex items-center gap-3 bg-slate-50">
|
||||
<i className="fas fa-list-ul text-indigo-500"></i>
|
||||
<h4 className="text-[11px] font-black text-slate-900 uppercase tracking-widest">All Events at {selectedVenue}</h4>
|
||||
<span className="ml-auto px-3 py-1 bg-indigo-100 text-indigo-700 text-[8px] font-black rounded-full uppercase">{venueEvents.length} Total</span>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="bg-slate-50/60 border-b border-slate-100">
|
||||
{['#', 'Event', 'Day', 'Date', 'Batches & Timings', 'Duration', 'Coordinator', 'Status'].map(h => (
|
||||
<th key={h} className="text-left px-5 py-3 text-[8px] font-black text-slate-400 uppercase tracking-widest whitespace-nowrap">{h}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-50">
|
||||
{venueEvents.map((ev, idx) => {
|
||||
let dayName = '—';
|
||||
try { const d = new Date(ev.date); if (!isNaN(d.getTime())) dayName = DAY_NAMES[d.getDay()]; } catch { /* skip */ }
|
||||
const batches = getBatches(ev);
|
||||
return (
|
||||
<tr key={ev.id} className="hover:bg-indigo-50/20 transition-colors">
|
||||
<td className="px-5 py-4 text-[10px] font-black text-slate-300">{idx + 1}</td>
|
||||
<td className="px-5 py-4 text-[11px] font-black text-slate-900 uppercase max-w-[140px]">
|
||||
<p className="truncate">{ev.title}</p>
|
||||
<p className="text-[8px] font-bold text-slate-400 normal-case">{ev.category}</p>
|
||||
</td>
|
||||
<td className="px-5 py-4 text-[10px] font-bold text-indigo-600 uppercase whitespace-nowrap">{dayName}</td>
|
||||
<td className="px-5 py-4 text-[10px] font-bold text-slate-500 whitespace-nowrap">{ev.date || '—'}</td>
|
||||
<td className="px-5 py-4">
|
||||
{batches.length > 0 ? (
|
||||
<div className="space-y-1.5">
|
||||
{batches.map((b, bi) => (
|
||||
<div key={bi} className="flex items-center gap-2 flex-wrap">
|
||||
<span className="px-2 py-0.5 bg-indigo-50 border border-indigo-200 text-indigo-700 text-[7px] font-black rounded-full uppercase whitespace-nowrap">{b.label}</span>
|
||||
<span className="text-[9px] font-bold text-slate-600 whitespace-nowrap">{b.time}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-[9px] text-slate-300 font-bold">—</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-5 py-4 text-[10px] font-bold text-slate-500 whitespace-nowrap">{ev.durationDays || 1} Day{(ev.durationDays || 1) > 1 ? 's' : ''}</td>
|
||||
<td className="px-5 py-4 text-[10px] font-bold text-slate-600 uppercase">{ev.coordinator || '—'}</td>
|
||||
<td className="px-5 py-4">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className={`w-2 h-2 rounded-full ${dotCls(ev.verificationStatus)}`}></span>
|
||||
<span className={`text-[8px] font-black uppercase tracking-widest ${txtCls(ev.verificationStatus)}`}>{ev.verificationStatus}</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
return <TrackVenueView />;
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#F3F4F6] text-slate-900 font-inter relative overflow-hidden">
|
||||
{/* Background Layer */}
|
||||
<div className="fixed inset-0 z-0 overflow-hidden">
|
||||
<div
|
||||
className="absolute inset-0 bg-cover bg-center"
|
||||
style={{ backgroundImage: 'url("https://cache.careers360.mobi/media/presets/720X480/colleges/social-media/media-gallery/3425/2021/6/16/DSC08602.JPG")' }}
|
||||
></div>
|
||||
<div className="absolute inset-0 bg-white/90 backdrop-blur-md"></div>
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 flex min-h-screen">
|
||||
<aside className={`w-24 md:w-64 border-r border-slate-100 bg-white/60 backdrop-blur-2xl flex flex-col transition-all duration-1000 ${isLoaded ? 'translate-x-0' : '-translate-x-full'}`}>
|
||||
<div className="p-8 border-b border-slate-100 flex flex-col items-center gap-4">
|
||||
<img
|
||||
src="https://www.facultyplus.com/wp-content/uploads/2020/11/Rit-logo.png"
|
||||
alt="RIT Logo"
|
||||
className="h-12 w-auto object-contain transition-all duration-300"
|
||||
/>
|
||||
<div className="hidden md:flex flex-col items-center leading-none">
|
||||
<span className="font-black text-xs tracking-tight text-[#004a99]">ADMIN PORTAL</span>
|
||||
<span className="text-[7px] text-slate-400 font-bold uppercase tracking-[0.2em] mt-1 text-center">Management Console</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav className="flex-1 py-10 px-4 space-y-2">
|
||||
{menuItems.map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
onClick={() => setActiveView(item.id)}
|
||||
className={`w-full flex items-center gap-4 px-6 py-4 rounded-[1.5rem] transition-all group ${activeView === item.id
|
||||
? 'bg-[#004a99] text-white shadow-xl shadow-[#004a99]/30'
|
||||
: 'text-slate-400 hover:text-[#004a99] hover:bg-white/50'
|
||||
}`}
|
||||
>
|
||||
<i className={`fas ${item.icon} text-lg`}></i>
|
||||
<span className="hidden md:block font-black text-[10px] uppercase tracking-widest">{item.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<div className="p-6 border-t border-slate-100">
|
||||
<button
|
||||
onClick={onLogout}
|
||||
className="w-full flex items-center justify-center gap-4 px-4 py-4 rounded-2xl text-rose-500 hover:bg-rose-50 transition-all font-black text-[10px] uppercase tracking-widest"
|
||||
>
|
||||
<i className="fas fa-right-from-bracket"></i>
|
||||
<span className="hidden md:block">Logout</span>
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main className="flex-1 overflow-y-auto custom-scrollbar relative">
|
||||
<div className="p-10 min-h-screen">
|
||||
{renderView()}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{toastMsg && <Toast message={toastMsg} onClose={handleCloseToast} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FacultyDashboard;
|
||||
155
RIT-EVENT-MANAGEMENT--main/components/FacultyNotifications.tsx
Normal file
155
RIT-EVENT-MANAGEMENT--main/components/FacultyNotifications.tsx
Normal file
@@ -0,0 +1,155 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { supabase } from '../supabase';
|
||||
import { Announcement } from '../types';
|
||||
|
||||
const FacultyNotifications: React.FC = () => {
|
||||
const [announcements, setAnnouncements] = useState<Announcement[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [now, setNow] = useState(new Date());
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => setNow(new Date()), 30000);
|
||||
return () => clearInterval(timer);
|
||||
}, []);
|
||||
|
||||
// Fetch announcements directly from Supabase — no prop dependency
|
||||
useEffect(() => {
|
||||
const fetchAnnouncements = async () => {
|
||||
setIsLoading(true);
|
||||
const { data, error } = await supabase
|
||||
.from('announcements')
|
||||
.select('*')
|
||||
.order('timestamp', { ascending: false });
|
||||
|
||||
if (data && !error) {
|
||||
setAnnouncements(data.map(ann => ({
|
||||
id: String(ann.id),
|
||||
title: ann.title,
|
||||
message: ann.message,
|
||||
type: ann.type,
|
||||
eventId: ann.event_id ? String(ann.event_id) : undefined,
|
||||
timestamp: ann.timestamp || ann.created_at || new Date().toISOString(),
|
||||
expiresAt: ann.expires_at
|
||||
})));
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
fetchAnnouncements();
|
||||
|
||||
// Subscribe to realtime changes on announcements
|
||||
const channel = supabase
|
||||
.channel('announcements-faculty')
|
||||
.on('postgres_changes', { event: '*', schema: 'public', table: 'announcements' }, () => {
|
||||
fetchAnnouncements();
|
||||
})
|
||||
.subscribe();
|
||||
|
||||
return () => { supabase.removeChannel(channel); };
|
||||
}, []);
|
||||
|
||||
const getTypeIcon = (type: string) => {
|
||||
switch (type) {
|
||||
case 'URGENT': return 'fa-triangle-exclamation text-rose-500';
|
||||
case 'DELAY': return 'fa-clock text-amber-500';
|
||||
case 'INFO': return 'fa-info-circle text-blue-500';
|
||||
case 'ENDED': return 'fa-flag-checkered text-purple-500';
|
||||
case 'ONGOING': return 'fa-play-circle text-emerald-500';
|
||||
default: return 'fa-bullhorn text-[#f97316]';
|
||||
}
|
||||
};
|
||||
|
||||
const formatTime = (timestamp: any): string => {
|
||||
if (!timestamp) return 'Now';
|
||||
try {
|
||||
// Supabase returns ISO strings — parse directly
|
||||
const date = new Date(timestamp);
|
||||
if (isNaN(date.getTime())) return 'Now';
|
||||
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
} catch {
|
||||
return 'Now';
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (timestamp: any): string => {
|
||||
if (!timestamp) return '';
|
||||
try {
|
||||
const date = new Date(timestamp);
|
||||
if (isNaN(date.getTime())) return '';
|
||||
const today = new Date();
|
||||
const isToday = date.toDateString() === today.toDateString();
|
||||
if (isToday) return 'Today';
|
||||
return date.toLocaleDateString([], { day: 'numeric', month: 'short' });
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
const isExpired = (expiresAt: any): boolean => {
|
||||
if (!expiresAt) return false;
|
||||
try {
|
||||
return new Date(expiresAt) < now;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const activeAnnouncements = announcements.filter(ann => !isExpired(ann.expiresAt));
|
||||
|
||||
return (
|
||||
<div className="animate-in fade-in slide-in-from-right-10 duration-700 max-w-4xl">
|
||||
<div className="mb-12">
|
||||
<h3 className="text-4xl font-black tracking-tight uppercase mb-2">Notice <span className="text-blue-500">Board</span></h3>
|
||||
<p className="text-[10px] font-black text-gray-500 uppercase tracking-[0.4em]">Administrative Updates & Alerts</p>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="py-20 flex flex-col items-center justify-center">
|
||||
<div className="w-12 h-12 border-4 border-blue-500/20 border-t-blue-500 rounded-full animate-spin mb-4"></div>
|
||||
<p className="text-[10px] font-black text-gray-600 uppercase tracking-widest">Syncing Notices...</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
{activeAnnouncements.length > 0 ? activeAnnouncements.map((n) => (
|
||||
<div key={n.id} className="bg-white/5 border border-white/5 rounded-[2.5rem] p-8 flex items-start justify-between group hover:bg-white/[0.08] transition-all cursor-pointer">
|
||||
<div className="flex items-start gap-8">
|
||||
<div className="w-16 h-16 rounded-[1.5rem] bg-white/5 flex items-center justify-center border border-white/10 group-hover:border-blue-500/50 transition-colors">
|
||||
<i className={`fas ${getTypeIcon(n.type)} text-xl`}></i>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-lg font-black uppercase tracking-tight group-hover:text-blue-500 transition-colors">{n.title}</h4>
|
||||
<p className="text-gray-400 text-sm font-medium mt-1 mb-3">{n.message}</p>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`w-1.5 h-1.5 rounded-full ${n.type === 'URGENT' ? 'bg-rose-500 animate-ping' : n.type === 'ENDED' ? 'bg-purple-500' : n.type === 'ONGOING' ? 'bg-emerald-500 animate-pulse' : 'bg-blue-500 animate-pulse'}`}></div>
|
||||
<span className={`text-[9px] font-black uppercase tracking-widest ${n.type === 'URGENT' ? 'text-rose-500' : n.type === 'ENDED' ? 'text-purple-500' : n.type === 'ONGOING' ? 'text-emerald-500' : 'text-blue-500/70'}`}>
|
||||
{n.type === 'ENDED' ? 'Status: Concluded' : n.type === 'ONGOING' ? 'Status: Live' : `Priority ${n.type === 'URGENT' ? 'Critical' : 'Regular'}`}
|
||||
</span>
|
||||
</div>
|
||||
{n.expiresAt && (
|
||||
<div className="flex items-center gap-2 text-rose-500/40">
|
||||
<i className="fas fa-hourglass-end text-[8px]"></i>
|
||||
<span className="text-[8px] font-black uppercase tracking-widest">Temporal Notice</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col items-end gap-1 shrink-0 ml-4">
|
||||
<span className="text-[10px] font-bold text-gray-600 uppercase tracking-widest">{formatTime(n.timestamp)}</span>
|
||||
<span className="text-[9px] font-bold text-gray-700 uppercase tracking-widest">{formatDate(n.timestamp)}</span>
|
||||
</div>
|
||||
</div>
|
||||
)) : (
|
||||
<div className="py-20 text-center bg-white/5 rounded-[2.5rem] border border-dashed border-white/10">
|
||||
<i className="fas fa-inbox text-4xl text-white/10 mb-4"></i>
|
||||
<p className="text-[10px] font-black text-gray-600 uppercase tracking-widest">No active notices</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FacultyNotifications;
|
||||
@@ -0,0 +1,639 @@
|
||||
import React, { useState, useMemo, useEffect } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { Event } from '../types';
|
||||
import { CATEGORIES, DOMAIN_MAP } from '../constants';
|
||||
import { supabase } from '../supabase';
|
||||
import * as XLSX from 'xlsx-js-style';
|
||||
|
||||
type ParticipantsSubView = 'CATEGORIES' | 'DOMAINS' | 'EVENTS' | 'STUDENTS';
|
||||
interface FacultyParticipantsViewProps {
|
||||
events: Event[];
|
||||
localRegistrations?: any[];
|
||||
currentUserId?: string;
|
||||
}
|
||||
|
||||
const FacultyParticipantsView: React.FC<FacultyParticipantsViewProps> = ({ events, localRegistrations = [], currentUserId }) => {
|
||||
const [currentView, setCurrentView] = useState<ParticipantsSubView>('STUDENTS');
|
||||
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
|
||||
const [selectedDomain, setSelectedDomain] = useState<string | null>(null);
|
||||
const [selectedEventId, setSelectedEventId] = useState<string | null>(null);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [showEventFilter, setShowEventFilter] = useState(false);
|
||||
const [showSideFilter, setShowSideFilter] = useState(false);
|
||||
const [selectedDepts, setSelectedDepts] = useState<string[]>([]);
|
||||
const [selectedYears, setSelectedYears] = useState<string[]>([]);
|
||||
const [activeFilterTab, setActiveFilterTab] = useState<'DEPT' | 'YEAR'>('DEPT');
|
||||
const [registrations, setRegistrations] = useState<any[]>(localRegistrations);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [selectedStudentIds, setSelectedStudentIds] = useState<Set<string>>(new Set());
|
||||
const [studentTypeFilter, setStudentTypeFilter] = useState<'INTERNAL' | 'EXTERNAL'>('INTERNAL');
|
||||
const [externalUserIds, setExternalUserIds] = useState<Set<string>>(new Set());
|
||||
|
||||
// Fetch registrations directly from Supabase for reliability
|
||||
useEffect(() => {
|
||||
const fetchRegistrations = async () => {
|
||||
setIsLoading(true);
|
||||
let { data, error } = await supabase
|
||||
.from('participants')
|
||||
.select('*')
|
||||
.order('created_at', { ascending: false });
|
||||
|
||||
if (error) {
|
||||
// Fallback to registrations if participants table doesn't exist yet
|
||||
const fallback = await supabase
|
||||
.from('registrations')
|
||||
.select('*')
|
||||
.order('registered_at', { ascending: false });
|
||||
data = fallback.data;
|
||||
error = fallback.error;
|
||||
}
|
||||
|
||||
if (data && !error) {
|
||||
setRegistrations(data);
|
||||
} else if (localRegistrations.length > 0) {
|
||||
setRegistrations(localRegistrations);
|
||||
}
|
||||
|
||||
const { data: extUsers } = await supabase.from('externalusers').select('id');
|
||||
if (extUsers) {
|
||||
setExternalUserIds(new Set(extUsers.map(u => u.id)));
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
fetchRegistrations();
|
||||
}, []);
|
||||
|
||||
const departments = ['CSBS', 'AIDS', 'AIML', 'ECE', 'VLSI', 'H&S', 'CCE', 'CSE', 'MECH', 'BIO-TECH', 'Information Technology (IT)', 'Electrical & Electronics Engineering (EEE)', 'Civil Engineering', 'Biomedical Engineering', 'Chemical Engineering', 'Aeronautical / Aerospace Engineering', 'Mechatronics Engineering', 'Others'];
|
||||
const years = ['1st Year', '2nd Year', '3rd Year', '4th Year', '5th Year'];
|
||||
|
||||
const handleCategorySelect = (id: string) => {
|
||||
setSelectedCategory(id);
|
||||
setCurrentView('DOMAINS');
|
||||
};
|
||||
|
||||
const handleDomainSelect = (id: string) => {
|
||||
setSelectedDomain(id);
|
||||
setCurrentView('EVENTS');
|
||||
};
|
||||
|
||||
const handleEventSelect = (id: string) => {
|
||||
setSelectedEventId(id);
|
||||
setCurrentView('STUDENTS');
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
if (currentView === 'STUDENTS') {
|
||||
setSelectedEventId(null);
|
||||
setCurrentView('EVENTS');
|
||||
} else if (currentView === 'EVENTS') {
|
||||
setSelectedDomain(null);
|
||||
setCurrentView('DOMAINS');
|
||||
} else if (currentView === 'DOMAINS') {
|
||||
setSelectedCategory(null);
|
||||
setCurrentView('CATEGORIES');
|
||||
}
|
||||
};
|
||||
|
||||
const filteredParticipants = useMemo(() => {
|
||||
return registrations.filter(r => {
|
||||
const userName = r.user_name || '';
|
||||
const regNo = r.reg_no || '';
|
||||
const searchLower = searchTerm.toLowerCase();
|
||||
|
||||
const matchesSearch = !searchTerm ||
|
||||
userName.toLowerCase().includes(searchLower) ||
|
||||
regNo.toLowerCase().includes(searchLower);
|
||||
|
||||
const matchesEvent = !selectedEventId || String(r.event_id) === String(selectedEventId);
|
||||
const matchesDept = selectedDepts.length === 0 || (r.dept && selectedDepts.includes(r.dept));
|
||||
const matchesYear = selectedYears.length === 0 || (r.year && selectedYears.includes(r.year));
|
||||
|
||||
const isExternal = r.college && r.college !== 'RAJALAKSHMI INSTITUTE OF TECHNOLOGY' && r.college !== 'Rajalakshmi Institute of Technology';
|
||||
const matchesType = studentTypeFilter === 'EXTERNAL' ? isExternal : !isExternal;
|
||||
|
||||
return matchesSearch && matchesEvent && matchesDept && matchesYear && matchesType;
|
||||
});
|
||||
}, [registrations, selectedEventId, searchTerm, selectedDepts, selectedYears, studentTypeFilter, externalUserIds]);
|
||||
|
||||
const getEventTitle = (eventId: string) => {
|
||||
return events.find(e => e.id === eventId)?.title || 'Unknown Event';
|
||||
};
|
||||
|
||||
const handleExportSelected = () => {
|
||||
if (selectedStudentIds.size === 0) return;
|
||||
|
||||
const selectedData = filteredParticipants.filter(p => selectedStudentIds.has(p.id));
|
||||
|
||||
// Formatting data for Excel
|
||||
const formattedData = selectedData.map(p => ({
|
||||
'Student Name': p.user_name || 'N/A',
|
||||
'Reg No': String(p.reg_no || 'N/A'),
|
||||
'Gender': p.gender || 'N/A',
|
||||
'College': p.college || 'RIT',
|
||||
'Department': p.dept || 'N/A',
|
||||
'Section': p.section || 'N/A',
|
||||
'Year': p.year || 'N/A',
|
||||
'Email': p.user_id?.includes('@') ? p.user_id : (p.user_email || 'N/A'),
|
||||
'Phone': String(p.phone || 'N/A'),
|
||||
'Event': p.event_name || getEventTitle(String(p.event_id)),
|
||||
'Team': p.team_name || 'N/A',
|
||||
'Payment Status': p.payment_status || 'PENDING',
|
||||
'Cert Status': p.certification_status === 'APPROVED' ? 'VERIFIED' : (p.certification_url ? 'PENDING' : 'NO CERT'),
|
||||
'OD Status': p.od_url ? 'OD READY' : 'NO OD'
|
||||
}));
|
||||
|
||||
// Create worksheet
|
||||
const worksheet = XLSX.utils.json_to_sheet(formattedData);
|
||||
|
||||
// Auto-fit columns
|
||||
if (formattedData.length > 0) {
|
||||
const keys = Object.keys(formattedData[0]);
|
||||
const wscols = keys.map(key => ({
|
||||
wch: Math.max(
|
||||
key.length,
|
||||
...formattedData.map(row => String(row[key as keyof typeof row] || '').length)
|
||||
) + 2
|
||||
}));
|
||||
worksheet['!cols'] = wscols;
|
||||
}
|
||||
|
||||
// Create workbook and export
|
||||
const workbook = XLSX.utils.book_new();
|
||||
XLSX.utils.book_append_sheet(workbook, worksheet, 'Participants');
|
||||
|
||||
XLSX.writeFile(workbook, `Participants_Export_${new Date().getTime()}.xlsx`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="animate-in slide-in-from-bottom-10 duration-500 pt-12">
|
||||
<div className="flex flex-col md:flex-row md:items-end justify-between mb-8 gap-8 border-b border-slate-200 pb-8">
|
||||
<div>
|
||||
<h3 className="text-4xl font-black tracking-tighter uppercase mb-2 text-slate-900">
|
||||
SYSTEM <span className="text-[#004a99]">REGISTRY</span>
|
||||
</h3>
|
||||
<p className="text-[10px] font-black text-slate-500 uppercase tracking-[0.4em]">Live Database Dossier Sync</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
{currentView !== 'CATEGORIES' && (
|
||||
<button
|
||||
onClick={handleBack}
|
||||
className="text-slate-600 font-black uppercase tracking-widest text-[10px] bg-slate-50 px-6 py-3 rounded-full border border-slate-200 hover:bg-slate-100 hover:text-slate-900 shadow-sm transition-all"
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-10 flex flex-col md:flex-row gap-4">
|
||||
<div className="relative flex-1">
|
||||
<i className="fas fa-search absolute left-6 top-1/2 -translate-y-1/2 text-slate-400"></i>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search registry..."
|
||||
className="w-full bg-white border border-slate-200 rounded-2xl pl-14 pr-6 py-4 text-slate-900 focus:ring-2 focus:ring-[#004a99] outline-none font-bold placeholder:text-slate-400 transition-all shadow-sm"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setShowEventFilter(!showEventFilter)}
|
||||
className="h-full px-6 py-4 bg-white border border-slate-200 rounded-2xl flex items-center gap-3 text-slate-700 font-bold hover:bg-slate-50 hover:border-slate-300 transition-all min-w-[240px] justify-between shadow-sm"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<i className="fas fa-filter text-[#004a99] text-xs"></i>
|
||||
<span className="text-xs uppercase tracking-widest truncate max-w-[150px]">
|
||||
{selectedEventId ? events.find(e => e.id === selectedEventId)?.title : 'All Events'}
|
||||
</span>
|
||||
</div>
|
||||
<i className={`fas fa-chevron-down text-[10px] transition-transform ${showEventFilter ? 'rotate-180' : ''}`}></i>
|
||||
</button>
|
||||
|
||||
{showEventFilter && (
|
||||
<div className="absolute top-full right-0 mt-2 w-80 bg-white border border-slate-200 rounded-2xl shadow-xl z-50 py-2 max-h-[400px] overflow-y-auto custom-scrollbar animate-in fade-in slide-in-from-top-2">
|
||||
<button
|
||||
onClick={() => { setSelectedEventId(null); setShowEventFilter(false); setCurrentView('STUDENTS'); }}
|
||||
className={`w-full text-left px-6 py-3 text-[10px] font-black uppercase tracking-widest hover:bg-slate-50 transition-all ${!selectedEventId ? 'text-[#004a99] bg-blue-50/50' : 'text-slate-600'}`}
|
||||
>
|
||||
All Events
|
||||
</button>
|
||||
<div className="h-px bg-slate-100 my-2" />
|
||||
{events.map(event => (
|
||||
<button
|
||||
key={event.id}
|
||||
onClick={() => { setSelectedEventId(event.id); setShowEventFilter(false); setCurrentView('STUDENTS'); }}
|
||||
className={`w-full text-left px-6 py-3 text-[10px] font-black uppercase tracking-widest hover:bg-slate-50 transition-all ${selectedEventId === event.id ? 'text-[#004a99] bg-blue-50/50' : 'text-slate-600'}`}
|
||||
>
|
||||
{event.title}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => setShowSideFilter(true)}
|
||||
className="px-6 py-4 bg-white border border-slate-200 rounded-2xl text-slate-700 font-black uppercase text-[10px] tracking-widest hover:bg-slate-50 transition-all flex items-center gap-2 relative shadow-sm"
|
||||
>
|
||||
<i className="fas fa-sliders-h text-[#004a99]"></i>
|
||||
Filters
|
||||
{(selectedDepts.length > 0 || selectedYears.length > 0) && (
|
||||
<span className="absolute -top-1 -right-1 w-4 h-4 bg-[#004a99] rounded-full text-[8px] flex items-center justify-center text-white shadow-md">
|
||||
{selectedDepts.length + selectedYears.length}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
setCurrentView(currentView === 'CATEGORIES' ? 'STUDENTS' : 'CATEGORIES');
|
||||
setSelectedEventId(null);
|
||||
setSearchTerm('');
|
||||
}}
|
||||
className="px-6 py-4 bg-[#004a99] text-white rounded-2xl font-black uppercase text-[10px] tracking-widest hover:bg-blue-800 transition-all flex items-center gap-2 shadow-md hover:shadow-lg active:scale-95"
|
||||
>
|
||||
<i className={`fas ${currentView === 'CATEGORIES' ? 'fa-list' : 'fa-grid-2'}`}></i>
|
||||
{currentView === 'CATEGORIES' ? 'List View' : 'Browse System'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 mb-8">
|
||||
<button
|
||||
onClick={() => setStudentTypeFilter('INTERNAL')}
|
||||
className={`flex-1 py-4 rounded-2xl border transition-all flex items-center justify-center gap-3 ${studentTypeFilter === 'INTERNAL' ? 'bg-blue-50 border-[#004a99] text-[#004a99] shadow-sm' : 'bg-white border-slate-200 text-slate-500 hover:border-blue-300'}`}
|
||||
>
|
||||
<i className="fas fa-university text-sm"></i>
|
||||
<span className="font-black uppercase tracking-widest text-[10px]">Internal Students</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setStudentTypeFilter('EXTERNAL')}
|
||||
className={`flex-1 py-4 rounded-2xl border transition-all flex items-center justify-center gap-3 ${studentTypeFilter === 'EXTERNAL' ? 'bg-orange-50 border-orange-500 text-orange-600 shadow-sm' : 'bg-white border-slate-200 text-slate-500 hover:border-orange-300'}`}
|
||||
>
|
||||
<i className="fas fa-globe text-sm"></i>
|
||||
<span className="font-black uppercase tracking-widest text-[10px]">External Students</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{currentView === 'CATEGORIES' && !searchTerm && (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8">
|
||||
{CATEGORIES.map((cat) => (
|
||||
<div key={cat.id} onClick={() => handleCategorySelect(cat.id)} className="group relative h-[400px] rounded-[3rem] overflow-hidden cursor-pointer bg-slate-100 border border-slate-200 hover:border-blue-300 hover:shadow-xl transition-all shadow-sm">
|
||||
<img src={cat.image} className="w-full h-full object-cover opacity-60 group-hover:scale-105 transition-transform duration-700" alt={cat.name} />
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent"></div>
|
||||
<div className="absolute bottom-10 left-10">
|
||||
<h4 className="text-3xl font-black text-white uppercase tracking-tighter">{cat.name}</h4>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{currentView === 'DOMAINS' && (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{(DOMAIN_MAP[selectedCategory!] || []).map((domain) => (
|
||||
<div key={domain.id} onClick={() => handleDomainSelect(domain.id)} className="group relative h-[350px] rounded-[2.5rem] overflow-hidden cursor-pointer bg-slate-100 border border-slate-200 hover:border-blue-300 hover:shadow-xl transition-all shadow-sm">
|
||||
<img src={domain.image} className="w-full h-full object-cover opacity-60 group-hover:scale-105 transition-transform duration-700" alt={domain.name} />
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent"></div>
|
||||
<div className="absolute bottom-10 left-10"><h4 className="text-2xl font-black text-white uppercase tracking-tight">{domain.name}</h4></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{currentView === 'EVENTS' && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{events.filter(e => (!selectedCategory || selectedCategory === 'ALL' || e.category === selectedCategory) && (!selectedDomain || selectedDomain === 'ALL' || e.domain === selectedDomain)).map((event) => (
|
||||
<div key={event.id} onClick={() => handleEventSelect(event.id)} className="bg-white border border-slate-200 rounded-[2.5rem] p-8 cursor-pointer hover:border-blue-300 hover:shadow-md transition-all flex flex-col h-full shadow-sm">
|
||||
<h4 className="text-xl font-black uppercase mb-4 text-slate-900 line-clamp-2">{event.title}</h4>
|
||||
<div className="mt-auto flex items-center justify-between">
|
||||
<p className="text-[10px] font-black text-slate-500 uppercase tracking-widest">
|
||||
{registrations.filter(r => String(r.event_id) === String(event.id)).length} Enrolled
|
||||
</p>
|
||||
<div className="w-10 h-10 rounded-full bg-slate-50 flex items-center justify-center text-[#004a99] group-hover:bg-blue-50 transition-colors">
|
||||
<i className="fas fa-arrow-right"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<FilterDrawer
|
||||
isOpen={showSideFilter}
|
||||
onClose={() => setShowSideFilter(false)}
|
||||
activeTab={activeFilterTab}
|
||||
setActiveTab={setActiveFilterTab}
|
||||
departments={departments}
|
||||
years={years}
|
||||
selectedDepts={selectedDepts}
|
||||
setSelectedDepts={setSelectedDepts}
|
||||
selectedYears={selectedYears}
|
||||
setSelectedYears={setSelectedYears}
|
||||
/>
|
||||
|
||||
{(currentView === 'STUDENTS' || searchTerm) && (
|
||||
<div className="space-y-8">
|
||||
{isLoading ? (
|
||||
<div className="col-span-full py-32 flex flex-col items-center justify-center text-center bg-white border border-dashed border-slate-200 rounded-[3rem]">
|
||||
<div className="w-16 h-16 border-4 border-blue-100 border-t-[#004a99] rounded-full animate-spin mb-6"></div>
|
||||
<p className="text-slate-400 font-black uppercase tracking-[0.3em] text-sm">Loading Registrations...</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-white border border-slate-200 rounded-[2.5rem] overflow-hidden shadow-sm">
|
||||
<div className="overflow-x-auto custom-scrollbar">
|
||||
<table className="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-slate-50 border-b border-slate-200">
|
||||
<th className="px-8 py-6 w-10">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="w-4 h-4 rounded border-slate-300 text-[#004a99] focus:ring-[#004a99] cursor-pointer"
|
||||
checked={filteredParticipants.length > 0 && filteredParticipants.every(p => selectedStudentIds.has(p.id))}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
setSelectedStudentIds(new Set(filteredParticipants.map(p => p.id)));
|
||||
} else {
|
||||
setSelectedStudentIds(new Set());
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</th>
|
||||
<th className="px-8 py-6 text-[10px] font-black tracking-widest text-slate-400 uppercase whitespace-nowrap">Student</th>
|
||||
<th className="px-8 py-6 text-[10px] font-black tracking-widest text-slate-400 uppercase whitespace-nowrap">Event Info</th>
|
||||
<th className="px-8 py-6 text-[10px] font-black tracking-widest text-slate-400 uppercase whitespace-nowrap">Academic</th>
|
||||
<th className="px-8 py-6 text-[10px] font-black tracking-widest text-slate-400 uppercase whitespace-nowrap">Contact</th>
|
||||
<th className="px-8 py-6 text-[10px] font-black tracking-widest text-slate-400 uppercase whitespace-nowrap text-center">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-100">
|
||||
{filteredParticipants.length > 0 ? filteredParticipants.map((p) => (
|
||||
<tr key={p.id} className="hover:bg-slate-50/50 transition-colors group">
|
||||
<td className="px-8 py-6 w-10">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="w-4 h-4 rounded border-slate-300 text-[#004a99] focus:ring-[#004a99] cursor-pointer"
|
||||
checked={selectedStudentIds.has(p.id)}
|
||||
onChange={(e) => {
|
||||
const newSet = new Set(selectedStudentIds);
|
||||
if (e.target.checked) newSet.add(p.id);
|
||||
else newSet.delete(p.id);
|
||||
setSelectedStudentIds(newSet);
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<td className="px-8 py-6 whitespace-nowrap">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 rounded-2xl bg-blue-50 text-[#004a99] flex items-center justify-center font-black text-lg border border-blue-100 shadow-sm">
|
||||
{(p.user_name || 'N').charAt(0).toUpperCase()}
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="text-sm font-black text-slate-900 uppercase tracking-tight group-hover:text-[#004a99] transition-colors">{p.user_name || 'N/A'}</p>
|
||||
{(externalUserIds.has(p.user_id) || !!p.college) && (
|
||||
<span className="bg-orange-100 text-orange-600 text-[7px] font-black px-1.5 py-0.5 rounded-md border border-orange-200 uppercase tracking-tighter">External</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest mt-1">{p.reg_no || 'N/A'}</p>
|
||||
{p.college && (
|
||||
<p className="text-[9px] font-black text-orange-500 uppercase tracking-widest mt-0.5 flex items-center gap-1">
|
||||
<i className="fas fa-school text-[8px]"></i> {p.college}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-8 py-6 whitespace-nowrap">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-full bg-orange-50 text-orange-500 flex items-center justify-center border border-orange-100 shrink-0">
|
||||
<i className="fas fa-calendar-alt text-[10px]"></i>
|
||||
</div>
|
||||
<p className="text-xs font-bold text-slate-700 truncate max-w-[200px] uppercase">{p.event_name || getEventTitle(String(p.event_id))}</p>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-8 py-6 whitespace-nowrap">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<div className="flex items-center gap-2">
|
||||
<i className="fas fa-university text-slate-400 text-[10px]"></i>
|
||||
<p className="text-[10px] font-black text-slate-700 uppercase tracking-wider">{p.dept || 'N/A'} {p.section ? `• SEC ${p.section}` : ''}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 pl-[18px]">
|
||||
<p className="text-[9px] font-bold text-slate-400 uppercase tracking-widest">{p.year || 'N/A'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-8 py-6 whitespace-nowrap">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-7 h-7 rounded-full bg-slate-100 text-slate-400 flex items-center justify-center shrink-0">
|
||||
<i className="fas fa-envelope text-[9px]"></i>
|
||||
</div>
|
||||
<p className="text-[11px] font-bold text-slate-600 lowercase tracking-wide">
|
||||
{p.user_id?.includes('@') ? p.user_id : (p.user_email || 'N/A')}
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-8 py-6 whitespace-nowrap">
|
||||
<div className="flex flex-col gap-1.5 items-center justify-center">
|
||||
<span className={`text-[7px] font-black px-2 py-1 rounded-md uppercase tracking-widest border w-24 text-center shadow-sm ${p.payment_status === 'COMPLETED' ? 'bg-emerald-50 text-emerald-600 border-emerald-200' : 'bg-rose-50 text-rose-600 border-rose-200'}`}>
|
||||
<i className={`fas mr-1 ${p.payment_status === 'COMPLETED' ? 'fa-check' : 'fa-clock'}`}></i> {p.payment_status || 'PENDING'}
|
||||
</span>
|
||||
<span className={`text-[7px] font-black px-2 py-1 rounded-md uppercase tracking-widest border w-24 text-center shadow-sm ${p.certification_status === 'APPROVED' ? 'bg-blue-50 text-[#004a99] border-blue-200' : (p.certification_url ? 'bg-amber-50 text-amber-600 border-amber-200' : 'bg-slate-50 text-slate-400 border-slate-200')}`}>
|
||||
<i className={`fas mr-1 ${p.certification_status === 'APPROVED' ? 'fa-check-double' : (p.certification_url ? 'fa-spinner' : 'fa-certificate')}`}></i> {p.certification_status === 'APPROVED' ? 'VERIFIED' : (p.certification_url ? 'PENDING' : 'NO CERT')}
|
||||
</span>
|
||||
<span className={`text-[7px] font-black px-2 py-1 rounded-md uppercase tracking-widest border w-24 text-center shadow-sm ${p.od_url ? 'bg-teal-50 text-teal-600 border-teal-200' : 'bg-slate-50 text-slate-400 border-slate-200'}`}>
|
||||
<i className={`fas mr-1 ${p.od_url ? 'fa-file-signature' : 'fa-file-excel'}`}></i> {p.od_url ? 'OD READY' : 'NO OD'}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)) : (
|
||||
<tr>
|
||||
<td colSpan={6} className="px-8 py-32 text-center">
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<div className="w-20 h-20 bg-slate-50 border border-slate-200 rounded-full flex items-center justify-center mb-6 shadow-sm">
|
||||
<i className="fas fa-search text-slate-300 text-2xl"></i>
|
||||
</div>
|
||||
<p className="text-slate-400 font-black uppercase tracking-[0.3em] text-sm">No active registrations for this scope</p>
|
||||
<p className="text-slate-500 text-[10px] font-bold uppercase tracking-widest mt-2 mb-8">Try adjusting your search or filters</p>
|
||||
<button
|
||||
onClick={() => {
|
||||
setSearchTerm('');
|
||||
setSelectedEventId(null);
|
||||
setSelectedDepts([]);
|
||||
setSelectedYears([]);
|
||||
}}
|
||||
className="px-8 py-3 bg-white border border-slate-200 rounded-xl text-[#004a99] font-black uppercase text-[10px] tracking-widest hover:bg-slate-50 hover:border-slate-300 transition-all shadow-sm"
|
||||
>
|
||||
Clear All Filters
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedStudentIds.size > 0 && (
|
||||
<div className="fixed bottom-10 left-1/2 -translate-x-1/2 z-[1000] flex justify-center animate-in slide-in-from-bottom-10 pointer-events-none">
|
||||
<div className="bg-white/95 backdrop-blur-xl border border-slate-200 shadow-2xl rounded-full px-8 py-4 flex items-center gap-6 pointer-events-auto ring-1 ring-[#004a99]/10">
|
||||
<span className="text-xs font-black text-slate-600 uppercase tracking-widest">
|
||||
{selectedStudentIds.size} Selected
|
||||
</span>
|
||||
<button
|
||||
onClick={handleExportSelected}
|
||||
className="bg-emerald-600 text-white px-8 py-3 rounded-full text-[10px] font-black uppercase tracking-widest hover:bg-emerald-700 transition-all shadow-lg shadow-emerald-600/20 active:scale-95 flex items-center gap-2"
|
||||
>
|
||||
<i className="fas fa-file-excel"></i> Export as Excel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const FilterDrawer: React.FC<{
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
activeTab: 'DEPT' | 'YEAR';
|
||||
setActiveTab: (tab: 'DEPT' | 'YEAR') => void;
|
||||
departments: string[];
|
||||
years: string[];
|
||||
selectedDepts: string[];
|
||||
setSelectedDepts: (depts: string[]) => void;
|
||||
selectedYears: string[];
|
||||
setSelectedYears: (years: string[]) => void;
|
||||
}> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
activeTab,
|
||||
setActiveTab,
|
||||
departments,
|
||||
years,
|
||||
selectedDepts,
|
||||
setSelectedDepts,
|
||||
selectedYears,
|
||||
setSelectedYears
|
||||
}) => {
|
||||
if (!isOpen) return null;
|
||||
|
||||
const toggleDept = (dept: string) => {
|
||||
if (selectedDepts.includes(dept)) {
|
||||
setSelectedDepts(selectedDepts.filter(d => d !== dept));
|
||||
} else {
|
||||
setSelectedDepts([...selectedDepts, dept]);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleYear = (year: string) => {
|
||||
if (selectedYears.includes(year)) {
|
||||
setSelectedYears(selectedYears.filter(y => y !== year));
|
||||
} else {
|
||||
setSelectedYears([...selectedYears, year]);
|
||||
}
|
||||
};
|
||||
|
||||
const clearAll = () => {
|
||||
setSelectedDepts([]);
|
||||
setSelectedYears([]);
|
||||
};
|
||||
|
||||
return createPortal(
|
||||
<div className="fixed inset-0 z-[9999] flex justify-end">
|
||||
<div className="absolute inset-0 bg-slate-900/40 backdrop-blur-sm" onClick={onClose}></div>
|
||||
<div className="relative w-full max-w-md bg-white h-full shadow-2xl flex flex-col animate-in slide-in-from-right duration-300">
|
||||
<div className="p-8 border-b border-slate-100 flex items-center justify-between">
|
||||
<div>
|
||||
<h4 className="text-xl font-black text-slate-900 uppercase tracking-tight">Refine Results</h4>
|
||||
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest mt-1">Select multiple filters</p>
|
||||
</div>
|
||||
<button onClick={onClose} className="w-10 h-10 rounded-full bg-slate-50 border border-slate-200 flex items-center justify-center text-slate-400 hover:text-slate-900 hover:bg-slate-100 transition-all">
|
||||
<i className="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 flex overflow-hidden">
|
||||
{/* Sidebar Tabs */}
|
||||
<div className="w-32 border-r border-slate-100 bg-slate-50/50">
|
||||
<button
|
||||
onClick={() => setActiveTab('DEPT')}
|
||||
className={`w-full py-6 px-4 text-left relative transition-all ${activeTab === 'DEPT' ? 'bg-blue-50 text-blue-800' : 'text-slate-500 hover:text-slate-700 hover:bg-slate-100/50'}`}
|
||||
>
|
||||
<span className="text-[10px] font-black uppercase tracking-widest">Department</span>
|
||||
{activeTab === 'DEPT' && <div className="absolute right-0 top-0 bottom-0 w-1 bg-[#004a99]"></div>}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('YEAR')}
|
||||
className={`w-full py-6 px-4 text-left relative transition-all ${activeTab === 'YEAR' ? 'bg-blue-50 text-blue-800' : 'text-slate-500 hover:text-slate-700 hover:bg-slate-100/50'}`}
|
||||
>
|
||||
<span className="text-[10px] font-black uppercase tracking-widest">Year</span>
|
||||
{activeTab === 'YEAR' && <div className="absolute right-0 top-0 bottom-0 w-1 bg-[#004a99]"></div>}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Options Content */}
|
||||
<div className="flex-1 p-8 overflow-y-auto custom-scrollbar">
|
||||
{activeTab === 'DEPT' ? (
|
||||
<div className="space-y-4">
|
||||
{departments.map(dept => (
|
||||
<label key={dept} className="flex items-center gap-4 group cursor-pointer">
|
||||
<div
|
||||
onClick={() => toggleDept(dept)}
|
||||
className={`w-6 h-6 rounded-lg border-2 flex items-center justify-center transition-all ${selectedDepts.includes(dept) ? 'bg-[#004a99] border-[#004a99] text-white' : 'border-slate-200 bg-white group-hover:border-blue-400'}`}
|
||||
>
|
||||
{selectedDepts.includes(dept) && <i className="fas fa-check text-[10px]"></i>}
|
||||
</div>
|
||||
<span className={`text-xs font-black uppercase tracking-widest transition-colors ${selectedDepts.includes(dept) ? 'text-slate-900' : 'text-slate-500'}`}>
|
||||
{dept}
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{years.map(year => (
|
||||
<label key={year} className="flex items-center gap-4 group cursor-pointer">
|
||||
<div
|
||||
onClick={() => toggleYear(year)}
|
||||
className={`w-6 h-6 rounded-lg border-2 flex items-center justify-center transition-all ${selectedYears.includes(year) ? 'bg-[#004a99] border-[#004a99] text-white' : 'border-slate-200 bg-white group-hover:border-blue-400'}`}
|
||||
>
|
||||
{selectedYears.includes(year) && <i className="fas fa-check text-[10px]"></i>}
|
||||
</div>
|
||||
<span className={`text-xs font-black uppercase tracking-widest transition-colors ${selectedYears.includes(year) ? 'text-slate-900' : 'text-slate-500'}`}>
|
||||
{year}
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-8 border-t border-slate-100 bg-slate-50 flex gap-4">
|
||||
<button
|
||||
onClick={clearAll}
|
||||
className="flex-1 py-4 bg-white border border-slate-200 rounded-2xl text-[10px] font-black text-slate-500 uppercase tracking-widest hover:bg-slate-100 transition-all shadow-sm"
|
||||
>
|
||||
Clear All
|
||||
</button>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="flex-1 py-4 bg-[#004a99] text-white rounded-2xl text-[10px] font-black uppercase tracking-widest hover:bg-blue-800 transition-all shadow-md active:scale-95"
|
||||
>
|
||||
Apply Filters
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
);
|
||||
};
|
||||
|
||||
export default FacultyParticipantsView;
|
||||
150
RIT-EVENT-MANAGEMENT--main/components/FacultyProfileView.tsx
Normal file
150
RIT-EVENT-MANAGEMENT--main/components/FacultyProfileView.tsx
Normal file
@@ -0,0 +1,150 @@
|
||||
import React from 'react';
|
||||
|
||||
interface FacultyProfileViewProps {
|
||||
onLogout: () => void;
|
||||
name?: string;
|
||||
dept?: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
photo?: string;
|
||||
}
|
||||
|
||||
const FacultyProfileView: React.FC<FacultyProfileViewProps> = ({
|
||||
onLogout,
|
||||
name = 'Faculty Member',
|
||||
dept = 'Department',
|
||||
email = 'faculty@ritchennai.edu.in',
|
||||
phone = 'Not provided',
|
||||
photo
|
||||
}) => {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 pt-32 pb-12 px-4 sm:px-6 lg:px-8 font-inter animate-in fade-in duration-500">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
|
||||
{/* Left Column: Profile Card */}
|
||||
<div className="lg:col-span-1">
|
||||
<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-[#004a99] to-blue-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">
|
||||
{photo ? (
|
||||
<img src={photo} alt={name} className="w-full h-full object-cover" />
|
||||
) : (
|
||||
<div className="w-full h-full bg-gradient-to-br from-slate-100 to-slate-200 flex items-center justify-center">
|
||||
<i className="fas fa-user-tie text-5xl text-slate-400"></i>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-1 uppercase tracking-tight">{name}</h2>
|
||||
<p className="text-sm font-medium text-[#004a99] uppercase tracking-wider mb-4">Event Coordinator</p>
|
||||
<div className="flex items-center justify-center gap-2 text-gray-500 text-sm">
|
||||
<i className="fas fa-envelope"></i>
|
||||
<span className="truncate">{email}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 border-t border-gray-100 pt-6 mb-6">
|
||||
<div className="text-center">
|
||||
<div className="inline-flex items-center gap-2 px-3 py-1 bg-emerald-50 text-emerald-600 rounded-full text-[10px] font-black uppercase tracking-widest border border-emerald-100">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500 shadow-[0_0_8px_rgba(16,185,129,0.5)]"></span>
|
||||
Verified Authority
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={onLogout}
|
||||
className="w-full py-2.5 border border-rose-200 text-rose-600 rounded-xl font-medium hover:bg-rose-50 transition-colors text-sm flex items-center justify-center gap-2"
|
||||
>
|
||||
<i className="fas fa-sign-out-alt"></i> Sign Out
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Column: Details */}
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
<div className="bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden">
|
||||
<div className="px-6 py-5 border-b border-gray-100 flex justify-between items-center">
|
||||
<h3 className="text-lg font-bold text-gray-900">Faculty Information</h3>
|
||||
</div>
|
||||
|
||||
<div className="p-8">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-y-8 gap-x-12">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-500 uppercase tracking-wider mb-1">Full Name</label>
|
||||
<p className="text-base font-semibold text-gray-900 uppercase">{name}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-500 uppercase tracking-wider mb-1">Email Address</label>
|
||||
<p className="text-base font-semibold text-gray-900 break-all">{email}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-500 uppercase tracking-wider mb-1">Phone Number</label>
|
||||
<p className="text-base font-semibold text-gray-900">{phone}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-500 uppercase tracking-wider mb-1">Role</label>
|
||||
<p className="text-base font-semibold text-gray-900 flex items-center gap-2">
|
||||
<span className="w-2 h-2 rounded-full bg-[#004a99]"></span>
|
||||
INSTITUTIONAL FACULTY AUTHORITY
|
||||
</p>
|
||||
</div>
|
||||
<div className="md:col-span-2">
|
||||
<label className="block text-xs font-medium text-gray-500 uppercase tracking-wider mb-1">Department</label>
|
||||
<p className="text-base font-semibold text-gray-900 uppercase">{dept}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden">
|
||||
<div className="px-6 py-5 border-b border-gray-100">
|
||||
<h3 className="text-lg font-bold text-gray-900">System Permissions</h3>
|
||||
</div>
|
||||
<div className="p-8">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="flex items-start gap-4 p-4 rounded-xl bg-gray-50 border border-gray-100">
|
||||
<i className="fas fa-calendar-plus text-lg text-emerald-500 mt-1"></i>
|
||||
<div>
|
||||
<p className="text-sm font-bold text-gray-900">Create Events</p>
|
||||
<p className="text-xs text-gray-500 mt-1">Authorized to curate and list new institutional events.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-4 p-4 rounded-xl bg-gray-50 border border-gray-100">
|
||||
<i className="fas fa-users-cog text-lg text-blue-500 mt-1"></i>
|
||||
<div>
|
||||
<p className="text-sm font-bold text-gray-900">Manage Participants</p>
|
||||
<p className="text-xs text-gray-500 mt-1">Can review registrations and track attendance.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-4 p-4 rounded-xl bg-gray-50 border border-gray-100">
|
||||
<i className="fas fa-chart-line text-lg text-orange-500 mt-1"></i>
|
||||
<div>
|
||||
<p className="text-sm font-bold text-gray-900">View Analytics</p>
|
||||
<p className="text-xs text-gray-500 mt-1">Access to event performance and engagement data.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-4 p-4 rounded-xl bg-gray-50 border border-gray-100">
|
||||
<i className="fas fa-certificate text-lg text-purple-500 mt-1"></i>
|
||||
<div>
|
||||
<p className="text-sm font-bold text-gray-900">Issue Certificates</p>
|
||||
<p className="text-xs text-gray-500 mt-1">Authorized to issue digital certificates to attendees.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FacultyProfileView;
|
||||
@@ -0,0 +1,91 @@
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { StudentRequest } from '../types';
|
||||
|
||||
interface FacultyRequestsViewProps {
|
||||
onShowToast: (msg: string) => void;
|
||||
}
|
||||
|
||||
const FacultyRequestsView: React.FC<FacultyRequestsViewProps> = ({ onShowToast }) => {
|
||||
const [requests, setRequests] = useState<StudentRequest[]>([
|
||||
{ id: 'R1', studentName: 'Aditya Varma', rollNo: '21IT001', branch: 'Information Technology', eventName: 'Next-Gen AI Forum', eventId: '1', timestamp: '2h ago', status: 'PENDING' },
|
||||
{ id: 'R2', studentName: 'Priya Dharshini', rollNo: '21CS042', branch: 'Computer Science', eventName: 'Robo-Wars 2025', eventId: '6', timestamp: '4h ago', status: 'PENDING' },
|
||||
{ id: 'R3', studentName: 'Manoj Kumar', rollNo: '22ME015', branch: 'Mechanical Engineering', eventName: 'Robo-Wars 2025', eventId: '6', timestamp: '5h ago', status: 'PENDING' },
|
||||
{ id: 'R4', studentName: 'Sneha Reddy', rollNo: '21IT112', branch: 'Information Technology', eventName: 'Web Architecture 2025', eventId: '2', timestamp: '1d ago', status: 'PENDING' },
|
||||
]);
|
||||
|
||||
const handleAction = (id: string, action: 'APPROVED' | 'REJECTED') => {
|
||||
const req = requests.find(r => r.id === id);
|
||||
if (!req) return;
|
||||
|
||||
setRequests(prev => prev.filter(r => r.id !== id));
|
||||
onShowToast(`Request from ${req.studentName} has been ${action.toLowerCase()}.`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="animate-in slide-in-from-bottom-10 duration-700">
|
||||
<div className="flex items-end justify-between mb-12">
|
||||
<div>
|
||||
<h3 className="text-4xl font-black tracking-tight uppercase mb-2">Registration <span className="text-blue-500">Requests</span></h3>
|
||||
<p className="text-[10px] font-black text-gray-500 uppercase tracking-[0.4em]">Review & Approve Student Access</p>
|
||||
</div>
|
||||
<div className="bg-white/5 px-6 py-3 rounded-2xl border border-white/10 flex items-center gap-4">
|
||||
<span className="text-[10px] font-black uppercase tracking-widest text-gray-400">Total Pending:</span>
|
||||
<span className="text-xl font-black text-blue-500 tabular-nums">{requests.length}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{requests.length > 0 ? (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
{requests.map((req) => (
|
||||
<div key={req.id} className="bg-white/5 border border-white/10 rounded-[2.5rem] p-8 transition-all hover:bg-white/[0.08] group relative overflow-hidden">
|
||||
<div className="absolute top-0 right-0 p-8 opacity-5 group-hover:opacity-10 transition-opacity">
|
||||
<i className="fas fa-user-graduate text-8xl"></i>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start justify-between mb-8 relative z-10">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-14 h-14 rounded-2xl bg-blue-500/10 flex items-center justify-center text-blue-500">
|
||||
<i className="fas fa-user-edit"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-xl font-black uppercase tracking-tight">{req.studentName}</h4>
|
||||
<span className="text-[10px] font-bold text-gray-500 uppercase tracking-widest">{req.rollNo} • {req.branch}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-[9px] font-black text-gray-600 uppercase tracking-widest">{req.timestamp}</span>
|
||||
</div>
|
||||
|
||||
<div className="bg-black/40 rounded-2xl p-6 mb-8 border border-white/5 relative z-10">
|
||||
<span className="block text-[9px] font-black text-blue-500 uppercase tracking-widest mb-2">Requested Event</span>
|
||||
<p className="text-lg font-black uppercase tracking-tight">{req.eventName}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 relative z-10">
|
||||
<button
|
||||
onClick={() => handleAction(req.id, 'APPROVED')}
|
||||
className="flex-1 py-4 bg-emerald-600 text-white rounded-2xl font-black uppercase text-[10px] tracking-widest hover:bg-emerald-700 transition-all flex items-center justify-center gap-2"
|
||||
>
|
||||
<i className="fas fa-check-circle"></i> Approve
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleAction(req.id, 'REJECTED')}
|
||||
className="flex-1 py-4 bg-rose-600/10 border border-rose-600/20 text-rose-500 rounded-2xl font-black uppercase text-[10px] tracking-widest hover:bg-rose-600 hover:text-white transition-all flex items-center justify-center gap-2"
|
||||
>
|
||||
<i className="fas fa-times-circle"></i> Reject
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="py-32 flex flex-col items-center justify-center bg-white/5 border-2 border-dashed border-white/10 rounded-[3rem]">
|
||||
<i className="fas fa-inbox text-6xl text-white/10 mb-6"></i>
|
||||
<p className="text-white/40 font-black uppercase tracking-widest">No pending requests found</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FacultyRequestsView;
|
||||
166
RIT-EVENT-MANAGEMENT--main/components/Footer.tsx
Normal file
166
RIT-EVENT-MANAGEMENT--main/components/Footer.tsx
Normal file
@@ -0,0 +1,166 @@
|
||||
import React from 'react';
|
||||
import { Youtube, Instagram, Facebook, Linkedin, Twitter, MapPin, Phone, Mail, ExternalLink } from 'lucide-react';
|
||||
|
||||
const Footer: React.FC = () => {
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
const footerLinks = {
|
||||
col1: [
|
||||
{ name: 'About The College', href: 'https://ritchennai.org' },
|
||||
{ name: 'News Room', href: 'https://ritchennai.org' },
|
||||
{ name: 'Governing Council', href: 'https://ritchennai.org/about-governing-council.php' },
|
||||
{ name: 'Career @ RIT', href: 'https://ritchennai.org' },
|
||||
{ name: 'Blog', href: 'https://ritchennai.org' },
|
||||
{ name: 'In alliance with Rajalakshmi Eduverse (www.myeduverse.net) - Cancellation & Refund Policy', href: 'https://ritchennai.org' },
|
||||
],
|
||||
col2: [
|
||||
{ name: 'Courses', href: 'https://www.ritchennai.org/admission-courses-offered.php' },
|
||||
{ name: 'Admissions', href: 'https://www.ritchennai.org/admission-eligibility.php' },
|
||||
{ name: 'Eligibility', href: 'https://www.ritchennai.org/admission-eligibility.php' },
|
||||
{ name: 'Code of Conduct - Students', href: 'https://ritchennai.org' },
|
||||
{ name: 'Online Fees Payment', href: 'https://ritchennai.org' },
|
||||
{ name: 'Terms & Conditions', href: 'https://ritchennai.org' },
|
||||
],
|
||||
col3: [
|
||||
{ name: 'Library', href: 'https://ritchennai.org' },
|
||||
{ name: 'Hostel', href: 'https://ritchennai.org' },
|
||||
{ name: 'Sports', href: 'https://ritchennai.org' },
|
||||
{ name: 'Transport', href: 'https://ritchennai.org' },
|
||||
{ name: 'HR Manual', href: 'https://ritchennai.org' },
|
||||
{ name: 'Patent', href: 'https://ritchennai.org' },
|
||||
{ name: 'Audited Statements', href: 'https://ritchennai.org' },
|
||||
{ name: 'Shopping and Delivery Conditions', href: 'https://ritchennai.org' },
|
||||
],
|
||||
};
|
||||
|
||||
const socialLinks = [
|
||||
{ icon: Facebook, href: 'https://www.facebook.com/ritchennai/', color: 'hover:text-blue-600' },
|
||||
{ icon: Twitter, href: 'https://x.com/rit_chennai', color: 'hover:text-sky-500' },
|
||||
{ icon: Instagram, href: 'https://www.instagram.com/ritchennai/', color: 'hover:text-pink-600' },
|
||||
{ icon: Linkedin, href: 'https://www.linkedin.com/school/rajalakshmi-institute-of-technology/', color: 'hover:text-blue-700' },
|
||||
{ icon: Youtube, href: 'https://youtube.com/@rajalakshmiinstituteoftech4448?si=jRmCDzp9dbwBLVQI', color: 'hover:text-red-600' },
|
||||
];
|
||||
|
||||
return (
|
||||
<footer className="bg-white pt-20 pb-10 border-t border-gray-100 font-sans text-gray-600">
|
||||
<div className="max-w-7xl mx-auto px-6 md:px-12 lg:px-24">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-12 gap-12 lg:gap-8 mb-16">
|
||||
|
||||
{/* Brand and Social */}
|
||||
<div className="lg:col-span-4 space-y-8">
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm leading-relaxed max-w-xs">
|
||||
We are a leading Engineering college in India, Rajalakshmi Institutions. Imparting the Spirit of Excellence and Quality Technical Education that fosters Learning.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4">
|
||||
{socialLinks.map((social, index) => (
|
||||
<a
|
||||
key={index}
|
||||
href={social.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={`w-10 h-10 rounded-full border border-gray-200 flex items-center justify-center transition-all duration-300 hover:border-transparent hover:shadow-lg ${social.color} group`}
|
||||
>
|
||||
<social.icon size={18} className="group-hover:scale-110 transition-transform" />
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="pt-8">
|
||||
<img
|
||||
src="https://www.facultyplus.com/wp-content/uploads/2020/11/Rit-logo.png"
|
||||
alt="RIT Logo"
|
||||
className="h-16 w-auto object-contain"
|
||||
referrerPolicy="no-referrer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Links Columns */}
|
||||
<div className="lg:col-span-2 space-y-6 pt-2">
|
||||
<ul className="space-y-4">
|
||||
{footerLinks.col1.map((link) => (
|
||||
<li key={link.name}>
|
||||
<a href={link.href} className="text-sm font-medium hover:text-[#f97316] transition-colors leading-relaxed">{link.name}</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="lg:col-span-2 space-y-6 pt-2">
|
||||
<ul className="space-y-4">
|
||||
{footerLinks.col2.map((link) => (
|
||||
<li key={link.name}>
|
||||
<a href={link.href} className="text-sm font-medium hover:text-[#f97316] transition-colors leading-relaxed">{link.name}</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="lg:col-span-2 space-y-6 pt-2">
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
{footerLinks.col3.map((link) => (
|
||||
<a key={link.name} href={link.href} className="text-sm font-medium hover:text-[#f97316] transition-colors leading-relaxed">{link.name}</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Contact Info */}
|
||||
<div className="lg:col-span-2 space-y-8">
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-xs font-black text-[#1e3a8a] uppercase tracking-widest border-b-2 border-[#f97316] pb-2 w-fit">
|
||||
ADDRESS
|
||||
</h3>
|
||||
<div className="space-y-4 text-[13px]">
|
||||
<div>
|
||||
<p className="font-bold text-gray-800 mb-1">Rajalakshmi Institutions Head Office</p>
|
||||
<p># 69 New Avadi Road</p>
|
||||
<p>Kilpauk</p>
|
||||
<p>Chennai - 600 010.</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-bold text-gray-800 mb-1">College Campus</p>
|
||||
<p>Rajalakshmi Nagar</p>
|
||||
<p>Thandalam</p>
|
||||
<p>Tamil Nadu 602105</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-xs font-black text-[#1e3a8a] uppercase tracking-widest border-b-2 border-[#f97316] pb-2 w-fit">
|
||||
PHONE
|
||||
</h3>
|
||||
<p className="text-[13px]">44-26442472 / 44-26461316 / 44-26460124</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-xs font-black text-[#1e3a8a] uppercase tracking-widest border-b-2 border-[#f97316] pb-2 w-fit">
|
||||
EMAIL
|
||||
</h3>
|
||||
<a href="mailto:mail@ritchennai.edu.in" className="text-[13px] hover:text-[#f97316] transition-colors">
|
||||
mail@ritchennai.edu.in
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* Bottom Bar */}
|
||||
<div className="pt-10 border-t border-gray-100 flex flex-col md:flex-row justify-between items-center gap-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[11px] font-bold text-gray-400 ml-2">© {currentYear} Rajalakshmi Institute Of Technology</span>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-8 text-[11px] font-bold uppercase tracking-widest">
|
||||
{/* Links removed as per request */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
48
RIT-EVENT-MANAGEMENT--main/components/Hero.tsx
Normal file
48
RIT-EVENT-MANAGEMENT--main/components/Hero.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
|
||||
import React from 'react';
|
||||
|
||||
interface HeroProps {
|
||||
events?: any[];
|
||||
}
|
||||
|
||||
const Hero: React.FC<HeroProps> = () => {
|
||||
return (
|
||||
<section className="relative w-full h-screen flex items-center justify-center overflow-hidden bg-[#F3F4F6] font-sans select-none">
|
||||
{/* Optimized Video Background Container */}
|
||||
<div className="absolute inset-0 z-0 overflow-hidden">
|
||||
<video
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
preload="auto"
|
||||
className="w-full h-full object-cover scale-125 pointer-events-none"
|
||||
style={{ imageRendering: 'auto' }}
|
||||
>
|
||||
<source src="https://mhvdpopbbtllhvzcpqkf.supabase.co/storage/v1/object/public/HERO_SECTION_VIDEO/VN20260331_201749.mp4" type="video/mp4" />
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
</div>
|
||||
|
||||
{/* Centered Content */}
|
||||
<div className="relative z-20 text-center px-6 max-w-5xl">
|
||||
<h1 className="text-white text-6xl md:text-[8rem] font-serif mb-8 tracking-tight leading-[0.9] animate-in fade-in slide-in-from-bottom-20 duration-1000 drop-shadow-lg">
|
||||
The Future is <br /> Built Here
|
||||
</h1>
|
||||
<p className="text-white/90 text-sm md:text-xl font-sans uppercase tracking-[0.6em] animate-in fade-in slide-in-from-bottom-10 duration-1000 delay-500 drop-shadow-md">
|
||||
Welcome to the RIT Events Hub
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Scroll Down Arrow */}
|
||||
<div className="absolute bottom-12 left-1/2 -translate-x-1/2 z-20 animate-bounce cursor-pointer opacity-40 hover:opacity-100 transition-opacity">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<span className="text-[10px] text-white font-sans uppercase tracking-[0.3em] mb-2">Scroll Down</span>
|
||||
<i className="fas fa-chevron-down text-white text-xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Hero;
|
||||
164
RIT-EVENT-MANAGEMENT--main/components/HomeDashboard.tsx
Normal file
164
RIT-EVENT-MANAGEMENT--main/components/HomeDashboard.tsx
Normal file
@@ -0,0 +1,164 @@
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import { Event, Announcement, SpecialEvent } from '../types';
|
||||
import { motion, AnimatePresence } from 'motion/react';
|
||||
import SpecialEventsBanner from './SpecialEventsBanner';
|
||||
import AccreditationsSection from './AccreditationsSection';
|
||||
|
||||
interface HomeDashboardProps {
|
||||
events: Event[];
|
||||
announcements: Announcement[];
|
||||
onNavigateToEvents: () => void;
|
||||
specialEvents: SpecialEvent[];
|
||||
}
|
||||
|
||||
const HomeDashboard: React.FC<HomeDashboardProps> = ({ events = [], announcements = [], onNavigateToEvents, specialEvents = [] }) => {
|
||||
const [now, setNow] = useState(new Date());
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => setNow(new Date()), 30000);
|
||||
return () => clearInterval(timer);
|
||||
}, []);
|
||||
|
||||
const filteredAnnouncements = useMemo(() => {
|
||||
return (announcements || []).filter(ann => {
|
||||
if (!ann.expiresAt) return true;
|
||||
const expiry = new Date(ann.expiresAt);
|
||||
return expiry > now;
|
||||
});
|
||||
}, [announcements, now]);
|
||||
|
||||
// Generate stable random values for rotation and color based on announcement ID
|
||||
const getNoteStyle = (id: string) => {
|
||||
// Simple hash function for stability
|
||||
const hash = id.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0);
|
||||
const rotations = [-2, -1, 1, 2, 3, -3];
|
||||
const colors = [
|
||||
'bg-[#fdfd96]', // Classic Yellow
|
||||
'bg-[#ff7eb9]', // Hot Pink
|
||||
'bg-[#7afcff]', // Light Blue
|
||||
'bg-[#feff9c]', // Light Yellow
|
||||
'bg-[#fff740]' // Darker Yellow
|
||||
];
|
||||
|
||||
return {
|
||||
rotation: rotations[hash % rotations.length],
|
||||
color: colors[hash % colors.length]
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="px-6 md:px-12 lg:px-24 py-12 bg-transparent animate-in fade-in duration-700">
|
||||
<div
|
||||
onClick={onNavigateToEvents}
|
||||
className="relative w-full h-64 md:h-80 rounded-[2.5rem] overflow-hidden cursor-pointer group mb-12 shadow-xl shadow-gray-200/20 hover:shadow-2xl transition-all duration-500"
|
||||
>
|
||||
<div className="absolute inset-0 bg-black/40 group-hover:bg-black/30 transition-colors duration-500 z-10"></div>
|
||||
<img
|
||||
src="https://img.freepik.com/premium-photo/outdoor-music-festival-evening-ambiance-with-festive-lights-crowd_114541-6739.jpg"
|
||||
alt="Events"
|
||||
className="absolute inset-0 w-full h-full object-cover transition-transform duration-700 group-hover:scale-105"
|
||||
/>
|
||||
<div className="absolute inset-0 z-20 flex flex-col justify-center px-12 md:px-20">
|
||||
<div className="transform transition-transform duration-500 group-hover:translate-x-2 space-y-4">
|
||||
<h2 className="text-4xl md:text-6xl font-serif text-white font-bold tracking-tight drop-shadow-lg">
|
||||
Let's get started
|
||||
</h2>
|
||||
<div className="flex items-center gap-3 text-white/90 group-hover:text-[#f97316] transition-colors w-fit">
|
||||
<span className="text-sm font-bold uppercase tracking-widest bg-black/20 backdrop-blur-sm px-4 py-2 rounded-full border border-white/20 group-hover:bg-white group-hover:text-[#f97316] transition-all duration-300">
|
||||
Explore Events <i className="fas fa-arrow-right ml-2"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Notice Board Section */}
|
||||
<div className="w-full bg-[#e8e4c9] p-8 md:p-12 rounded-[2rem] shadow-2xl border-[12px] border-[#8d6e63] relative overflow-hidden min-h-[600px]">
|
||||
{/* Cork texture pattern */}
|
||||
<div className="absolute inset-0 opacity-30 bg-[url('https://www.transparenttextures.com/patterns/cork-board.png')] pointer-events-none"></div>
|
||||
|
||||
<div className="relative z-10 mb-12 flex flex-col md:flex-row items-start md:items-center justify-between gap-6 border-b-2 border-[#8d6e63]/20 pb-6">
|
||||
<div>
|
||||
<h2 className="text-4xl md:text-5xl font-serif text-[#3e2723] tracking-tight font-bold drop-shadow-sm mb-2">
|
||||
Campus Notice Board
|
||||
</h2>
|
||||
<p className="text-[#5d4037] font-medium opacity-80">Real-time updates from the administration</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 md:gap-12 p-6">
|
||||
<AnimatePresence mode="popLayout">
|
||||
{filteredAnnouncements.length > 0 ? (
|
||||
filteredAnnouncements.slice(0, 6).map((ann) => {
|
||||
const style = getNoteStyle(ann.id);
|
||||
return (
|
||||
<motion.div
|
||||
key={ann.id}
|
||||
layout
|
||||
initial={{ scale: 0.8, opacity: 0, y: 50, rotate: style.rotation + (Math.random() * 10 - 5) }}
|
||||
animate={{ scale: 1, opacity: 1, y: 0, rotate: style.rotation }}
|
||||
exit={{ scale: 0.8, opacity: 0, transition: { duration: 0.2 } }}
|
||||
whileHover={{ scale: 1.05, rotate: 0, zIndex: 50, transition: { duration: 0.2 } }}
|
||||
transition={{ type: "spring", stiffness: 300, damping: 20 }}
|
||||
className={`${style.color} p-8 shadow-[4px_4px_10px_rgba(0,0,0,0.2)] hover:shadow-[15px_15px_30px_rgba(0,0,0,0.3)] transition-shadow duration-300 relative group cursor-pointer min-h-[300px] flex flex-col transform-gpu`}
|
||||
>
|
||||
{/* Pin */}
|
||||
<div className="absolute -top-3 left-1/2 -translate-x-1/2 w-5 h-5 rounded-full bg-[#d32f2f] shadow-[2px_2px_4px_rgba(0,0,0,0.3)] z-20 border border-[#b71c1c] after:content-[''] after:absolute after:top-1 after:left-1 after:w-1.5 after:h-1.5 after:bg-white/50 after:rounded-full"></div>
|
||||
|
||||
<div className="mb-6 pt-2">
|
||||
<span className={`text-xs font-black uppercase tracking-widest px-3 py-1.5 rounded-sm ${
|
||||
ann.type === 'URGENT' ? 'bg-red-500/20 text-red-800' :
|
||||
ann.type === 'DELAY' ? 'bg-amber-500/20 text-amber-800' :
|
||||
'bg-black/5 text-gray-800'
|
||||
}`}>
|
||||
{ann.type || 'NOTICE'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h3 className="font-serif font-bold text-2xl text-gray-900 mb-4 leading-tight">
|
||||
{ann.title}
|
||||
</h3>
|
||||
|
||||
<p className="font-sans text-gray-800 text-base flex-grow mb-6 leading-relaxed opacity-90">
|
||||
{ann.message}
|
||||
</p>
|
||||
|
||||
<div className="mt-auto pt-4 border-t border-black/10 flex flex-col gap-2 text-[11px] font-bold text-gray-700 uppercase tracking-wider">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="flex items-center gap-2">
|
||||
<i className="far fa-calendar-alt"></i>
|
||||
{new Date(ann.timestamp).toLocaleDateString(undefined, { weekday: 'short', month: 'short', day: 'numeric' })}
|
||||
</span>
|
||||
<span className="flex items-center gap-2">
|
||||
<i className="far fa-clock"></i>
|
||||
{new Date(ann.timestamp).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}
|
||||
</span>
|
||||
</div>
|
||||
{ann.expiresAt && <span className="text-red-700/70 text-right">Exp: {new Date(ann.expiresAt).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}</span>}
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
className="col-span-full flex flex-col items-center justify-center py-32 text-[#8d6e63]/50"
|
||||
>
|
||||
<i className="fas fa-thumbtack text-6xl mb-6 opacity-30 rotate-45"></i>
|
||||
<p className="font-serif text-2xl font-bold">The board is empty right now.</p>
|
||||
<p className="font-sans text-sm mt-2 uppercase tracking-widest">Check back later for updates</p>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AccreditationsSection />
|
||||
|
||||
<SpecialEventsBanner specialEvents={specialEvents} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HomeDashboard;
|
||||
405
RIT-EVENT-MANAGEMENT--main/components/LoginForm.tsx
Normal file
405
RIT-EVENT-MANAGEMENT--main/components/LoginForm.tsx
Normal file
@@ -0,0 +1,405 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { UserRole } from '../types';
|
||||
import { supabase } from '../supabase';
|
||||
|
||||
const DEPARTMENTS = ['AIDS', 'CSBS', 'CSE', 'CCE', 'MECH', 'VLSI', 'BIO-TECH', 'AIML', 'ECE', 'H&S'];
|
||||
const EXTERNAL_DEPARTMENTS = [
|
||||
...DEPARTMENTS,
|
||||
'Information Technology (IT)',
|
||||
'Electrical & Electronics Engineering (EEE)',
|
||||
'Civil Engineering',
|
||||
'Biomedical Engineering',
|
||||
'Chemical Engineering',
|
||||
'Aeronautical / Aerospace Engineering',
|
||||
'Mechatronics Engineering',
|
||||
'Others'
|
||||
];
|
||||
const SECTIONS = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I'];
|
||||
const YEARS = ['1st Year', '2nd Year', '3rd Year', '4th Year', '5th Year'];
|
||||
|
||||
interface LoginFormProps {
|
||||
role: UserRole;
|
||||
onSuccess: () => void;
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
const LoginForm: React.FC<LoginFormProps> = ({ role, onSuccess, onBack }) => {
|
||||
const [isSignUp, setIsSignUp] = useState(false);
|
||||
const [signUpType, setSignUpType] = useState<'INTERNAL' | 'EXTERNAL' | null>(null);
|
||||
const [coordinatorLoginType, setCoordinatorLoginType] = useState<'Faculty' | 'HOD' | null>(null);
|
||||
const [formData, setFormData] = useState({
|
||||
email: '',
|
||||
phone: '',
|
||||
regNo: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
name: '',
|
||||
collegeName: 'Rajalakshmi Institute of Technology',
|
||||
department: '',
|
||||
section: '',
|
||||
year: '',
|
||||
gender: '',
|
||||
collegeLocation: '',
|
||||
captchaInput: '',
|
||||
});
|
||||
|
||||
// Auto-set internal for non-student roles
|
||||
useEffect(() => {
|
||||
if (role !== 'STUDENT') {
|
||||
setSignUpType('INTERNAL');
|
||||
} else if (!isSignUp) {
|
||||
setSignUpType(null);
|
||||
}
|
||||
}, [role, isSignUp]);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData(prev => ({ ...prev, [name]: value }));
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setErrorMessage(null);
|
||||
setIsSubmitting(true);
|
||||
|
||||
try {
|
||||
if (!isSignUp) {
|
||||
// Login Logic
|
||||
const { data: loginData, error } = await supabase.auth.signInWithPassword({
|
||||
email: formData.email,
|
||||
password: formData.password
|
||||
});
|
||||
if (error) throw error;
|
||||
|
||||
if (role === 'COORDINATOR' && coordinatorLoginType && loginData.user) {
|
||||
const { data: faculty } = await supabase.from('Facultyusers').select('role').eq('id', loginData.user.id).single();
|
||||
if (!faculty || (faculty.role !== coordinatorLoginType && faculty.role !== 'System Admin')) {
|
||||
await supabase.auth.signOut();
|
||||
throw new Error(`Access Denied: You are not provisioned as ${coordinatorLoginType}.`);
|
||||
}
|
||||
}
|
||||
|
||||
// For students, ensure they have a profile in Studentusers
|
||||
if (role === 'STUDENT' && signUpType === 'INTERNAL' && loginData.user) {
|
||||
const { data: existing } = await supabase.from('Studentusers').select('id').eq('id', loginData.user.id).single();
|
||||
if (!existing) {
|
||||
const { error: profileError } = await supabase.from('Studentusers').insert({
|
||||
id: loginData.user.id,
|
||||
name: formData.name,
|
||||
email: formData.email,
|
||||
reg_no: formData.regNo,
|
||||
phone: formData.phone,
|
||||
department: formData.department,
|
||||
year: formData.year,
|
||||
section: formData.section,
|
||||
college_name: 'RAJALAKSHMI INSTITUTE OF TECHNOLOGY'
|
||||
});
|
||||
if (profileError) console.error("Profile creation error:", profileError);
|
||||
}
|
||||
}
|
||||
|
||||
onSuccess();
|
||||
} else {
|
||||
// Sign-Up Logic
|
||||
const { data: signUpData, error } = await supabase.auth.signUp({
|
||||
email: formData.email,
|
||||
password: formData.password,
|
||||
options: {
|
||||
data: {
|
||||
name: formData.name,
|
||||
role: signUpType === 'EXTERNAL' ? 'EXTERNAL_STUDENT' : role,
|
||||
regNo: formData.regNo,
|
||||
phone: formData.phone,
|
||||
gender: formData.gender,
|
||||
college: formData.collegeName,
|
||||
collegeLocation: formData.collegeLocation,
|
||||
department: formData.department,
|
||||
year: formData.year,
|
||||
section: formData.section
|
||||
}
|
||||
}
|
||||
});
|
||||
if (error) throw error;
|
||||
|
||||
if (signUpData.user) {
|
||||
if (signUpType === 'EXTERNAL') {
|
||||
await supabase.from('externalusers').insert({
|
||||
id: signUpData.user.id,
|
||||
name: formData.name,
|
||||
email: formData.email,
|
||||
reg_no: formData.regNo,
|
||||
phone: formData.phone,
|
||||
department: formData.department,
|
||||
year: formData.year,
|
||||
section: formData.section,
|
||||
college: formData.collegeName,
|
||||
college_location: formData.collegeLocation,
|
||||
gender: formData.gender
|
||||
});
|
||||
} else if (role === 'STUDENT') {
|
||||
await supabase.from('Studentusers').insert({
|
||||
id: signUpData.user.id,
|
||||
name: formData.name,
|
||||
email: formData.email,
|
||||
reg_no: formData.regNo,
|
||||
phone: formData.phone,
|
||||
department: formData.department,
|
||||
year: formData.year,
|
||||
section: formData.section,
|
||||
college_name: 'RAJALAKSHMI INSTITUTE OF TECHNOLOGY'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
alert("Verification link sent to your email. Please verify to continue.");
|
||||
setIsSignUp(false);
|
||||
if (role === 'STUDENT') setSignUpType(null);
|
||||
}
|
||||
} catch (err: any) {
|
||||
setErrorMessage(err.message || "An error occurred.");
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const RIT_BLUE = 'bg-[#004a99]';
|
||||
const RIT_BLUE_TEXT = 'text-[#004a99]';
|
||||
const RIT_BLUE_HOVER = 'hover:bg-[#003366]';
|
||||
const RIT_BLUE_BORDER = 'border-[#004a99]';
|
||||
|
||||
return (
|
||||
<div className="min-h-screen w-full bg-slate-50 flex items-center justify-center p-4 md:p-10 font-inter">
|
||||
{/* Main Container */}
|
||||
<div className="relative w-full max-w-5xl h-[700px] bg-white rounded-[3rem] shadow-[0_50px_100px_-20px_rgba(0,0,0,0.15)] overflow-hidden flex flex-col md:flex-row border border-slate-100">
|
||||
|
||||
{/* Back Button */}
|
||||
<button
|
||||
onClick={onBack}
|
||||
className="absolute top-8 left-8 z-[60] w-12 h-12 bg-white/10 backdrop-blur-md border border-white/20 rounded-full flex items-center justify-center text-white hover:bg-white/20 transition-all"
|
||||
>
|
||||
<i className="fas fa-arrow-left"></i>
|
||||
</button>
|
||||
|
||||
{/* Forms Container */}
|
||||
<div className="relative flex-1 flex">
|
||||
|
||||
{/* Sign Up Form (Left Side) */}
|
||||
<div className={`absolute inset-0 w-full md:w-1/2 h-full flex flex-col justify-center p-12 transition-all duration-700 ease-in-out z-10 ${isSignUp ? 'opacity-100 translate-x-0 visible' : 'opacity-0 -translate-x-full invisible pointer-events-none'}`}>
|
||||
<img
|
||||
src="https://www.facultyplus.com/wp-content/uploads/2020/11/Rit-logo.png"
|
||||
alt="RIT Logo"
|
||||
className="h-12 w-auto mb-6 object-contain"
|
||||
/>
|
||||
{!signUpType && role === 'STUDENT' ? (
|
||||
<div className="space-y-6 text-center animate-in fade-in zoom-in-95 duration-500">
|
||||
<h2 className="text-3xl font-black text-slate-900 uppercase tracking-tighter mb-8">Choose <span className="text-orange-500">Portal</span></h2>
|
||||
<div className="grid gap-4">
|
||||
<button
|
||||
onClick={() => setSignUpType('INTERNAL')}
|
||||
className={`w-full py-6 ${RIT_BLUE} text-white rounded-2xl font-black uppercase text-xs tracking-[0.2em] shadow-lg transition-all hover:scale-[1.02] active:scale-95`}
|
||||
>
|
||||
RIT Student (Internal)
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setSignUpType('EXTERNAL')}
|
||||
className="w-full py-6 bg-white border-2 border-slate-200 text-slate-700 rounded-2xl font-black uppercase text-xs tracking-[0.2em] transition-all hover:border-orange-500 hover:text-orange-500 hover:scale-[1.02] active:scale-95"
|
||||
>
|
||||
Other College (External)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={handleSubmit} className="space-y-4 overflow-y-auto no-scrollbar py-4">
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-3xl font-black text-slate-900 uppercase tracking-tighter">
|
||||
{role === 'STUDENT' ? (signUpType === 'INTERNAL' ? 'Internal' : 'External') : (role === 'COORDINATOR' ? 'Event Coordinator' : 'Admin')} <span className="text-orange-500">Sign Up</span>
|
||||
</h2>
|
||||
{role === 'STUDENT' && <button type="button" onClick={() => setSignUpType(null)} className="text-[10px] font-black text-slate-400 uppercase hover:text-orange-500">Change</button>}
|
||||
</div>
|
||||
<p className="text-slate-400 text-[10px] font-bold uppercase tracking-widest mt-2">Join the RIT Excellence Hub</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<input required name="name" placeholder="FULL NAME" className="bg-slate-50 border-none rounded-2xl px-6 py-4 text-xs font-bold outline-none focus:ring-2 focus:ring-[#004a99] transition-all" value={formData.name} onChange={handleInputChange} />
|
||||
<input required type="email" name="email" placeholder="EMAIL" className="bg-slate-50 border-none rounded-2xl px-6 py-4 text-xs font-bold outline-none focus:ring-2 focus:ring-[#004a99] transition-all" value={formData.email} onChange={handleInputChange} />
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<input required name="regNo" placeholder="REG NO" className="bg-slate-50 border-none rounded-2xl px-6 py-4 text-xs font-bold outline-none focus:ring-2 focus:ring-[#004a99] transition-all" value={formData.regNo} onChange={handleInputChange} />
|
||||
<input required name="phone" placeholder="PHONE NUMBER" className="bg-slate-50 border-none rounded-2xl px-6 py-4 text-xs font-bold outline-none focus:ring-2 focus:ring-[#004a99] transition-all" value={formData.phone} onChange={handleInputChange} />
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<input required type="password" name="password" placeholder="PASSWORD" className="bg-slate-50 border-none rounded-2xl px-6 py-4 text-xs font-bold outline-none focus:ring-2 focus:ring-[#004a99] transition-all" value={formData.password} onChange={handleInputChange} />
|
||||
<input required type="password" name="confirmPassword" placeholder="CONFIRM" className="bg-slate-50 border-none rounded-2xl px-6 py-4 text-xs font-bold outline-none focus:ring-2 focus:ring-[#004a99] transition-all" value={formData.confirmPassword} onChange={handleInputChange} />
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<select required name="gender" className="bg-slate-50 border-none rounded-2xl px-6 py-4 text-xs font-bold outline-none focus:ring-2 focus:ring-[#004a99] transition-all" value={formData.gender} onChange={handleInputChange}>
|
||||
<option value="">SELECT GENDER</option>
|
||||
<option value="Male">MALE</option>
|
||||
<option value="Female">FEMALE</option>
|
||||
<option value="Other">OTHER</option>
|
||||
</select>
|
||||
<input required name="collegeLocation" placeholder="COLLEGE LOCATION" className="bg-slate-50 border-none rounded-2xl px-6 py-4 text-xs font-bold outline-none focus:ring-2 focus:ring-[#004a99] transition-all" value={formData.collegeLocation} onChange={handleInputChange} />
|
||||
</div>
|
||||
|
||||
{(role === 'STUDENT' || signUpType === 'EXTERNAL') && (
|
||||
<>
|
||||
<input
|
||||
required
|
||||
name="collegeName"
|
||||
placeholder="COLLEGE NAME"
|
||||
disabled={signUpType === 'INTERNAL'}
|
||||
className="w-full bg-slate-50 border-none rounded-2xl px-6 py-4 text-xs font-bold outline-none focus:ring-2 focus:ring-[#004a99] transition-all disabled:opacity-60"
|
||||
value={formData.collegeName}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<select required name="department" className="bg-slate-50 border-none rounded-2xl px-4 py-4 text-[10px] font-black outline-none" value={formData.department} onChange={handleInputChange}>
|
||||
<option value="">DEPT</option>
|
||||
{(signUpType === 'EXTERNAL' ? EXTERNAL_DEPARTMENTS : DEPARTMENTS).map(d => <option key={d} value={d}>{d}</option>)}
|
||||
</select>
|
||||
<select required name="year" className="bg-slate-50 border-none rounded-2xl px-4 py-4 text-[10px] font-black outline-none" value={formData.year} onChange={handleInputChange}>
|
||||
<option value="">YEAR</option>
|
||||
{YEARS.map(y => <option key={y} value={y}>{y}</option>)}
|
||||
</select>
|
||||
<select required name="section" className="bg-slate-50 border-none rounded-2xl px-4 py-4 text-[10px] font-black outline-none" value={formData.section} onChange={handleInputChange}>
|
||||
<option value="">SEC</option>
|
||||
{SECTIONS.map(s => <option key={s} value={s}>{s}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{errorMessage && <p className="text-rose-500 text-[10px] font-bold uppercase">{errorMessage}</p>}
|
||||
|
||||
<button type="submit" disabled={isSubmitting} className="w-full py-5 bg-orange-500 text-white text-[10px] font-black uppercase tracking-[0.3em] rounded-2xl hover:bg-orange-600 transition-all shadow-lg shadow-orange-500/20 active:scale-95">
|
||||
{isSubmitting ? 'Initializing...' : 'Sign Up Now'}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Sign In Form (Right Side) */}
|
||||
<div className={`absolute inset-0 w-full md:w-1/2 h-full flex flex-col justify-center p-12 transition-all duration-700 ease-in-out z-10 ml-auto ${!isSignUp ? 'opacity-100 translate-x-0 visible' : 'opacity-0 translate-x-full invisible pointer-events-none'}`}>
|
||||
{role === 'COORDINATOR' && !coordinatorLoginType ? (
|
||||
<div className="space-y-6 text-center animate-in fade-in zoom-in-95 duration-500">
|
||||
<img
|
||||
src="https://www.facultyplus.com/wp-content/uploads/2020/11/Rit-logo.png"
|
||||
alt="RIT Logo"
|
||||
className="h-12 w-auto mb-6 object-contain mx-auto"
|
||||
/>
|
||||
<h2 className="text-3xl font-black text-slate-900 uppercase tracking-tighter mb-8">Choose <span className="text-orange-500">Access Level</span></h2>
|
||||
<div className="grid gap-4">
|
||||
<button
|
||||
onClick={() => setCoordinatorLoginType('Faculty')}
|
||||
type="button"
|
||||
className={`w-full py-6 ${RIT_BLUE} text-white rounded-2xl font-black uppercase text-xs tracking-[0.2em] shadow-lg transition-all hover:scale-[1.02] active:scale-95`}
|
||||
>
|
||||
Faculty Member
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCoordinatorLoginType('HOD')}
|
||||
type="button"
|
||||
className="w-full py-6 bg-orange-500 text-white rounded-2xl font-black uppercase text-xs tracking-[0.2em] shadow-lg transition-all hover:scale-[1.02] active:scale-95"
|
||||
>
|
||||
Head of Department
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div className="mb-8">
|
||||
<img
|
||||
src="https://www.facultyplus.com/wp-content/uploads/2020/11/Rit-logo.png"
|
||||
alt="RIT Logo"
|
||||
className="h-12 w-auto mb-6 object-contain"
|
||||
/>
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-4xl font-black text-slate-900 uppercase tracking-tighter">Welcome <span className={RIT_BLUE_TEXT}>Back</span></h2>
|
||||
{role === 'COORDINATOR' && (
|
||||
<button type="button" onClick={() => setCoordinatorLoginType(null)} className="text-[10px] font-black text-slate-400 uppercase hover:text-orange-500">Change</button>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-slate-400 text-xs font-bold uppercase tracking-widest mt-2">
|
||||
Access your {role === 'ADMIN' ? 'Admin' : (role === 'COORDINATOR' ? `${coordinatorLoginType} Event` : 'Student')} portal
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="relative">
|
||||
<i className="fas fa-envelope absolute left-6 top-1/2 -translate-y-1/2 text-slate-300"></i>
|
||||
<input required type="email" name="email" placeholder="EMAIL ADDRESS" className="w-full bg-slate-50 border-none rounded-2xl pl-14 pr-6 py-5 text-xs font-bold outline-none focus:ring-2 focus:ring-[#004a99] transition-all" value={formData.email} onChange={handleInputChange} />
|
||||
</div>
|
||||
<div className="relative">
|
||||
<i className="fas fa-lock absolute left-6 top-1/2 -translate-y-1/2 text-slate-300"></i>
|
||||
<input required type={showPassword ? 'text' : 'password'} name="password" placeholder="PASSWORD" className="w-full bg-slate-50 border-none rounded-2xl pl-14 pr-14 py-5 text-xs font-bold outline-none focus:ring-2 focus:ring-[#004a99] transition-all" value={formData.password} onChange={handleInputChange} />
|
||||
<button type="button" onClick={() => setShowPassword(!showPassword)} className={`absolute right-6 top-1/2 -translate-y-1/2 text-slate-300 hover:${RIT_BLUE_TEXT} transition-colors`}>
|
||||
<i className={`fas ${showPassword ? 'fa-eye-slash' : 'fa-eye'}`}></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<button type="button" className="text-[10px] font-black text-slate-400 uppercase tracking-widest hover:text-orange-500 transition-colors">Forgot Password?</button>
|
||||
</div>
|
||||
|
||||
{errorMessage && <p className="text-rose-500 text-[10px] font-bold uppercase">{errorMessage}</p>}
|
||||
|
||||
<button type="submit" disabled={isSubmitting} className={`w-full py-5 ${RIT_BLUE} text-white text-[10px] font-black uppercase tracking-[0.3em] rounded-2xl ${RIT_BLUE_HOVER} transition-all shadow-lg shadow-blue-900/20 active:scale-95`}>
|
||||
{isSubmitting ? 'Verifying...' : 'Sign In Now'}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Sliding Overlay Panel */}
|
||||
<div className={`absolute top-0 left-0 w-full md:w-1/2 h-full ${RIT_BLUE} z-50 transition-all duration-700 ease-[cubic-bezier(0.7,0,0.3,1)] flex flex-col items-center justify-center text-center p-12 overflow-hidden ${isSignUp ? 'md:translate-x-full' : 'translate-x-0'}`}>
|
||||
{/* Background Pattern */}
|
||||
<div className="absolute inset-0 opacity-10 pointer-events-none">
|
||||
<div className="absolute top-0 left-0 w-full h-full bg-[radial-gradient(circle_at_center,_white_1px,_transparent_1px)] bg-[size:30px_30px]"></div>
|
||||
</div>
|
||||
|
||||
<div className={`absolute inset-0 flex flex-col items-center justify-center p-12 transition-all duration-700 delay-100 ${isSignUp ? 'opacity-0 -translate-y-10 pointer-events-none invisible' : 'opacity-100 translate-y-0 pointer-events-auto visible'}`}>
|
||||
<h2 className="text-4xl font-black text-white uppercase tracking-tighter mb-4">{role === 'STUDENT' ? 'New Here?' : 'Restricted Access'}</h2>
|
||||
<p className="text-white/80 text-sm font-medium leading-relaxed mb-10 max-w-xs mx-auto">
|
||||
{role === 'STUDENT'
|
||||
? 'Sign up and discover a world of possibilities at RIT Events Hub.'
|
||||
: 'Sign up is restricted for this portal. Please contact the administrator to provision an account.'}
|
||||
</p>
|
||||
{role === 'STUDENT' && (
|
||||
<button
|
||||
onClick={() => setIsSignUp(true)}
|
||||
className="px-12 py-4 border-2 border-white text-white text-[10px] font-black uppercase tracking-[0.3em] rounded-full hover:bg-white hover:text-[#004a99] transition-all active:scale-95"
|
||||
>
|
||||
Sign Up
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={`absolute inset-0 flex flex-col items-center justify-center p-12 transition-all duration-700 delay-100 ${(!isSignUp || role !== 'STUDENT') ? 'opacity-0 translate-y-10 pointer-events-none invisible' : 'opacity-100 translate-y-0 pointer-events-auto visible'}`}>
|
||||
<h2 className="text-4xl font-black text-white uppercase tracking-tighter mb-4">Welcome <span className="text-white/70">Back!</span></h2>
|
||||
<p className="text-white/80 text-sm font-medium leading-relaxed mb-10 max-w-xs mx-auto">
|
||||
To keep connected with us please login with your personal info.
|
||||
</p>
|
||||
<button
|
||||
onClick={() => setIsSignUp(false)}
|
||||
className="px-12 py-4 border-2 border-white text-white text-[10px] font-black uppercase tracking-[0.3em] rounded-full hover:bg-white hover:text-[#004a99] transition-all active:scale-95"
|
||||
>
|
||||
Sign In
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginForm;
|
||||
92
RIT-EVENT-MANAGEMENT--main/components/Navbar.tsx
Normal file
92
RIT-EVENT-MANAGEMENT--main/components/Navbar.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { DashboardView } from '../types';
|
||||
|
||||
interface NavbarProps {
|
||||
activeView: DashboardView;
|
||||
onViewChange: (view: DashboardView) => void;
|
||||
onLogout: () => void;
|
||||
onBackToCoordinatorHub?: () => void;
|
||||
}
|
||||
|
||||
const Navbar: React.FC<NavbarProps> = ({ activeView, onViewChange, onLogout, onBackToCoordinatorHub }) => {
|
||||
const [isScrolled, setIsScrolled] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
setIsScrolled(window.scrollY > 10);
|
||||
};
|
||||
window.addEventListener('scroll', handleScroll);
|
||||
return () => window.removeEventListener('scroll', handleScroll);
|
||||
}, []);
|
||||
|
||||
const menuItems: { id: DashboardView; label: string }[] = [
|
||||
{ id: 'HOME', label: 'HOME' },
|
||||
{ id: 'EVENTS', label: 'EVENTS' },
|
||||
{ id: 'REGISTRATIONS', label: 'REGISTRATIONS' },
|
||||
{ id: 'PROFILE', label: 'PROFILE' },
|
||||
{ id: 'ABOUT', label: 'ABOUT' },
|
||||
{ id: 'CONTACT', label: 'CONTACT' },
|
||||
];
|
||||
|
||||
return (
|
||||
<nav className={`fixed top-0 left-0 w-full z-[100] transition-all duration-500 px-8 py-5 flex items-center justify-between ${
|
||||
isScrolled
|
||||
? 'bg-white/90 backdrop-blur-2xl py-4 shadow-sm'
|
||||
: 'bg-transparent py-6'
|
||||
}`}>
|
||||
{/* Brand Logo (Top Left) */}
|
||||
<div
|
||||
className="flex items-center cursor-pointer group transition-transform hover:scale-105"
|
||||
onClick={() => onViewChange('HOME')}
|
||||
>
|
||||
<div className="transition-all duration-300">
|
||||
<img
|
||||
src="https://www.facultyplus.com/wp-content/uploads/2020/11/Rit-logo.png"
|
||||
alt="RIT Logo"
|
||||
className={`h-12 w-auto object-contain transition-all duration-300`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Nav Links & Logout (Right Side) */}
|
||||
<div className="flex items-center gap-8 md:gap-12">
|
||||
<div className="hidden md:flex items-center gap-8">
|
||||
{onBackToCoordinatorHub && (
|
||||
<button
|
||||
onClick={onBackToCoordinatorHub}
|
||||
className={`text-[10px] font-black tracking-[0.2em] px-4 py-1.5 rounded-lg transition-all border border-[#f97316] text-[#f97316] hover:bg-[#f97316] hover:text-white`}
|
||||
>
|
||||
COORDINATOR HUB
|
||||
</button>
|
||||
)}
|
||||
{menuItems.map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
onClick={() => onViewChange(item.id)}
|
||||
className={`text-xs font-serif tracking-[0.2em] transition-all relative py-1 ${
|
||||
activeView === item.id
|
||||
? 'text-[#f97316]'
|
||||
: isScrolled ? 'text-black hover:text-[#f97316]' : 'text-black hover:text-[#f97316]'
|
||||
}`}
|
||||
>
|
||||
{item.label}
|
||||
{activeView === item.id && (
|
||||
<span className="absolute -bottom-1 left-0 w-full h-[1px] bg-[#f97316]"></span>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={onLogout}
|
||||
className={`text-[10px] font-black tracking-[0.2em] px-6 py-2 rounded-full transition-all border bg-[#f97316] text-white border-[#f97316] hover:bg-[#ea580c] hover:border-[#ea580c]`}
|
||||
>
|
||||
LOG OUT
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
||||
export default Navbar;
|
||||
108
RIT-EVENT-MANAGEMENT--main/components/PortalAnimation.tsx
Normal file
108
RIT-EVENT-MANAGEMENT--main/components/PortalAnimation.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
|
||||
interface PortalAnimationProps {
|
||||
isVisible: boolean;
|
||||
onComplete: () => void;
|
||||
targetLink: string;
|
||||
}
|
||||
|
||||
const PortalAnimation: React.FC<PortalAnimationProps> = ({ isVisible, onComplete, targetLink }) => {
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
const [statusText, setStatusText] = useState("Initializing Port...");
|
||||
|
||||
useEffect(() => {
|
||||
if (isVisible) {
|
||||
// Sequence of status texts for a more high-tech feel
|
||||
const statusInterval = setInterval(() => {
|
||||
const texts = ["Synchronizing...", "Establishing Link...", "Calibrating Vortex...", "Dimensional Drift...", "Transmitting..."];
|
||||
setStatusText(prev => {
|
||||
const idx = texts.indexOf(prev);
|
||||
return texts[(idx + 1) % texts.length];
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
// 5-second delay for the high-quality transition
|
||||
const timer = setTimeout(() => {
|
||||
// Redirection logic starts - the page will unload
|
||||
// The overlay will naturally stay until the browser replaces the page
|
||||
window.location.href = targetLink;
|
||||
}, 5000);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timer);
|
||||
clearInterval(statusInterval);
|
||||
};
|
||||
}
|
||||
}, [isVisible, targetLink]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isVisible && videoRef.current) {
|
||||
videoRef.current.play().catch(err => console.error("Video play failed:", err));
|
||||
}
|
||||
}, [isVisible]);
|
||||
|
||||
if (!isVisible) return null;
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
// Using Fixed inset-0 and a massive z-index to cover EVERYTHING
|
||||
className="fixed inset-0 z-[999999] bg-black flex items-center justify-center overflow-hidden touch-none pointer-events-auto"
|
||||
>
|
||||
<div className="relative w-full h-full">
|
||||
{/* High-Quality Native Video - Absolute Full Screen */}
|
||||
<video
|
||||
ref={videoRef}
|
||||
src="/vortex.mp4"
|
||||
muted
|
||||
playsInline
|
||||
loop
|
||||
className="w-full h-full object-cover"
|
||||
style={{ width: '100vw', height: '100vh' }}
|
||||
/>
|
||||
|
||||
{/* Premium Overlay Filter */}
|
||||
<div className="absolute inset-0 bg-blue-900/20 mix-blend-overlay pointer-events-none"></div>
|
||||
|
||||
{/* Transition Status Text Overlay */}
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center pointer-events-none">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="text-center"
|
||||
>
|
||||
<div className="mb-4">
|
||||
<div className="w-64 h-[2px] bg-white/20 relative overflow-hidden mx-auto">
|
||||
<motion.div
|
||||
initial={{ x: "-100%" }}
|
||||
animate={{ x: "100%" }}
|
||||
transition={{ duration: 1.5, repeat: Infinity, ease: "linear" }}
|
||||
className="absolute inset-0 bg-orange-500 shadow-[0_0_10px_#f97316]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-[10px] font-black uppercase tracking-[0.8em] text-white/70">
|
||||
{statusText}
|
||||
</span>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Subliminal Transition Glow (Pulses just before redirect) */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: [0, 0, 0.6, 0] }}
|
||||
transition={{ duration: 5, times: [0, 0.85, 0.95, 1], ease: "easeInOut" }}
|
||||
className="absolute inset-0 bg-white pointer-events-none"
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
);
|
||||
};
|
||||
|
||||
export default PortalAnimation;
|
||||
420
RIT-EVENT-MANAGEMENT--main/components/ProfileView.tsx
Normal file
420
RIT-EVENT-MANAGEMENT--main/components/ProfileView.tsx
Normal file
@@ -0,0 +1,420 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { UserRole } from '../types';
|
||||
import { supabase, uploadToSupabase } from '../supabase';
|
||||
|
||||
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 = 400;
|
||||
const MAX_HEIGHT = 400;
|
||||
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));
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
interface ProfileViewProps {
|
||||
onLogout?: () => void;
|
||||
onSupabaseError?: () => void;
|
||||
}
|
||||
|
||||
const ProfileView: React.FC<ProfileViewProps> = ({ onLogout, onSupabaseError }) => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [userData, setUserData] = useState<any>(null);
|
||||
const [userRole, setUserRole] = useState<UserRole>('STUDENT');
|
||||
const [userTable, setUserTable] = useState<string>('Studentusers');
|
||||
const [regCount, setRegCount] = useState(0);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [editForm, setEditForm] = useState({ name: '', phone: '', profile_photo: '', year: '', section: '' });
|
||||
const [showSuccess, setShowSuccess] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const [pastEvents, setPastEvents] = useState<any[]>([]);
|
||||
|
||||
const fetchProfile = async () => {
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
if (!user) return;
|
||||
try {
|
||||
let profile = null;
|
||||
let role: UserRole = 'STUDENT';
|
||||
let table = 'Studentusers';
|
||||
|
||||
const HIGH_AUTH_ADMINS = [
|
||||
'adrit1highauth@gmail.com',
|
||||
'adrit2highauth@gmail.com',
|
||||
'adrit3highauth@gmail.com',
|
||||
'adrit4highauth@gmail.com',
|
||||
'adrit5highauth@gmail.com'
|
||||
];
|
||||
|
||||
if (user.email && HIGH_AUTH_ADMINS.includes(user.email)) {
|
||||
const { data: faculty } = await supabase.from('Facultyusers').select('*').eq('id', user.id).single();
|
||||
if (faculty) { profile = faculty; role = 'ADMIN'; table = 'Facultyusers'; }
|
||||
} else {
|
||||
const [studentRes, extRes, adminRes, facultyRes] = await Promise.all([
|
||||
supabase.from('Studentusers').select('*').eq('id', user.id).single(),
|
||||
supabase.from('externalusers').select('*').eq('id', user.id).single(),
|
||||
supabase.from('Adminusers').select('*').eq('id', user.id).single(),
|
||||
supabase.from('Facultyusers').select('*').eq('id', user.id).single()
|
||||
]);
|
||||
|
||||
if (studentRes.data) { profile = studentRes.data; role = 'STUDENT'; table = 'Studentusers'; }
|
||||
else if (extRes.data) { profile = extRes.data; role = 'STUDENT'; table = 'externalusers'; }
|
||||
else if (adminRes.data) { profile = adminRes.data; role = 'COORDINATOR'; table = 'Adminusers'; }
|
||||
else if (facultyRes.data) { profile = facultyRes.data; role = 'COORDINATOR'; table = 'Facultyusers'; }
|
||||
}
|
||||
|
||||
if (profile) {
|
||||
setUserData(profile);
|
||||
setUserRole(role);
|
||||
setUserTable(table);
|
||||
setEditForm({ name: profile.name || '', phone: profile.phone || '', profile_photo: profile.profile_photo || '', year: profile.year || '', section: profile.section || '' });
|
||||
}
|
||||
|
||||
// Fetch registration count & participation history concurrently
|
||||
const [countRes, historyRes] = await Promise.all([
|
||||
supabase.from('registrations').select('*', { count: 'exact', head: true }).eq('user_id', user.id),
|
||||
supabase.from('participants').select('*, events(title, category)').eq('user_id', user.id).order('participation_date', { ascending: false })
|
||||
]);
|
||||
|
||||
setRegCount(countRes.count || 0);
|
||||
if (historyRes.data) setPastEvents(historyRes.data);
|
||||
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => { fetchProfile(); }, []);
|
||||
|
||||
const handleUpdate = async (e?: React.FormEvent) => {
|
||||
if (e) e.preventDefault();
|
||||
setIsSaving(true);
|
||||
setErrorMessage(null);
|
||||
try {
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
if (!user) return;
|
||||
|
||||
let photoUrl = editForm.profile_photo;
|
||||
if (photoUrl.startsWith('data:')) {
|
||||
photoUrl = await uploadToSupabase(photoUrl, `avatars/${user.id}_${Date.now()}.jpg`, 'Profile');
|
||||
}
|
||||
|
||||
const { error } = await supabase.from(userTable).update({
|
||||
name: editForm.name,
|
||||
phone: editForm.phone,
|
||||
profile_photo: photoUrl,
|
||||
year: editForm.year,
|
||||
section: editForm.section
|
||||
}).eq('id', user.id);
|
||||
|
||||
if (error) throw error;
|
||||
setUserData({ ...userData, name: editForm.name, phone: editForm.phone, profile_photo: photoUrl, year: editForm.year, section: editForm.section });
|
||||
setIsEditing(false);
|
||||
setShowSuccess(true);
|
||||
setTimeout(() => setShowSuccess(false), 3000);
|
||||
} catch (err: any) {
|
||||
setErrorMessage(err.message);
|
||||
if (err.message?.includes('RLS')) onSupabaseError?.();
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePhotoChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = async () => {
|
||||
const compressed = await compressImage(reader.result as string);
|
||||
setEditForm({ ...editForm, profile_photo: compressed });
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) return <div className="h-screen flex items-center justify-center bg-[#F3F4F6]"><i className="fas fa-spinner fa-spin text-4xl text-[#f97316]"></i></div>;
|
||||
|
||||
const avatarUrl = isEditing ? (editForm.profile_photo || userData?.profile_photo) : userData?.profile_photo;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 pt-32 pb-12 px-4 sm:px-6 lg:px-8 font-inter">
|
||||
{showSuccess && createPortal(
|
||||
<div className="fixed inset-0 z-[1000] flex items-center justify-center p-6 bg-black/40 backdrop-blur-sm animate-in fade-in duration-300">
|
||||
<div className="bg-white rounded-2xl w-full max-w-sm p-8 shadow-2xl flex flex-col items-center text-center transform transition-all scale-100">
|
||||
<div className="w-16 h-16 bg-emerald-100 text-emerald-600 rounded-full flex items-center justify-center text-2xl mb-6">
|
||||
<i className="fas fa-check"></i>
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-gray-900 mb-2">Profile Updated</h3>
|
||||
<p className="text-gray-500 mb-6">Your changes have been saved successfully.</p>
|
||||
<button onClick={() => setShowSuccess(false)} className="w-full py-3 bg-gray-900 text-white rounded-xl font-semibold hover:bg-gray-800 transition-colors">
|
||||
Continue
|
||||
</button>
|
||||
</div>
|
||||
</div>, document.body
|
||||
)}
|
||||
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
{/* Left Column: Profile Card */}
|
||||
<div className="lg:col-span-1">
|
||||
<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=${userData?.name}`}
|
||||
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">{userData?.name}</h2>
|
||||
<p className="text-sm font-medium text-orange-500 uppercase tracking-wider mb-4">{userRole}</p>
|
||||
<div className="flex items-center justify-center gap-2 text-gray-500 text-sm">
|
||||
<i className="fas fa-envelope"></i>
|
||||
<span>{userData?.email}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 border-t border-gray-100 pt-6 mb-6">
|
||||
<div className="text-center">
|
||||
<span className="block text-xl font-bold text-gray-900">{regCount}</span>
|
||||
<span className="text-xs text-gray-500 uppercase tracking-wide">Registry</span>
|
||||
</div>
|
||||
<div className="text-center border-l border-gray-100">
|
||||
<span className="block text-xl font-bold text-emerald-600">{pastEvents.length}</span>
|
||||
<span className="text-xs text-gray-500 uppercase tracking-wide">Completed</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!isEditing && (
|
||||
<button
|
||||
onClick={onLogout}
|
||||
className="w-full py-2.5 border border-gray-200 text-gray-600 rounded-xl font-medium hover:bg-gray-50 hover:text-rose-600 transition-colors text-sm flex items-center justify-center gap-2"
|
||||
>
|
||||
<i className="fas fa-sign-out-alt"></i> Sign Out
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Column: Details & Edit Form */}
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
{/* Personal Information Card */}
|
||||
<div className="bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden">
|
||||
<div className="px-6 py-5 border-b border-gray-100 flex justify-between items-center">
|
||||
<h3 className="text-lg font-bold text-gray-900">Personal Information</h3>
|
||||
{!isEditing && userRole !== 'STUDENT' && (
|
||||
<button
|
||||
onClick={() => setIsEditing(true)}
|
||||
className="text-sm font-medium text-orange-600 hover:text-orange-700 flex items-center gap-1"
|
||||
>
|
||||
<i className="fas fa-pen"></i> Edit
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="p-6">
|
||||
{isEditing ? (
|
||||
<form onSubmit={handleUpdate} className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-gray-700">Full Name</label>
|
||||
<input
|
||||
type="text"
|
||||
value={editForm.name}
|
||||
onChange={e => setEditForm({...editForm, name: e.target.value})}
|
||||
className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl focus:ring-2 focus:ring-orange-500/20 focus:border-orange-500 outline-none transition-all"
|
||||
placeholder="Enter your full name"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-gray-700">Phone Number</label>
|
||||
<input
|
||||
type="tel"
|
||||
value={editForm.phone}
|
||||
onChange={e => setEditForm({...editForm, phone: e.target.value})}
|
||||
className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl focus:ring-2 focus:ring-orange-500/20 focus:border-orange-500 outline-none transition-all"
|
||||
placeholder="Enter your phone number"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-gray-700">Year</label>
|
||||
<select
|
||||
value={editForm.year}
|
||||
onChange={e => setEditForm({...editForm, year: e.target.value})}
|
||||
className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl focus:ring-2 focus:ring-orange-500/20 focus:border-orange-500 outline-none transition-all"
|
||||
>
|
||||
<option value="">Select Year</option>
|
||||
<option value="I">I</option>
|
||||
<option value="II">II</option>
|
||||
<option value="III">III</option>
|
||||
<option value="IV">IV</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-gray-700">Section</label>
|
||||
<input
|
||||
type="text"
|
||||
value={editForm.section}
|
||||
onChange={e => setEditForm({...editForm, section: e.target.value.toUpperCase()})}
|
||||
className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl focus:ring-2 focus:ring-orange-500/20 focus:border-orange-500 outline-none transition-all"
|
||||
placeholder="e.g. A, B, C"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{errorMessage && (
|
||||
<div className="p-3 bg-rose-50 text-rose-600 text-sm rounded-lg flex items-center gap-2">
|
||||
<i className="fas fa-exclamation-circle"></i>
|
||||
{errorMessage}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-4 pt-4">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSaving}
|
||||
className="px-6 py-2.5 bg-gray-900 text-white rounded-xl font-medium hover:bg-black transition-colors disabled:opacity-50 flex items-center gap-2"
|
||||
>
|
||||
{isSaving ? <i className="fas fa-spinner fa-spin"></i> : <i className="fas fa-save"></i>}
|
||||
{isSaving ? 'Saving...' : 'Save Changes'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { setIsEditing(false); setErrorMessage(null); }}
|
||||
className="px-6 py-2.5 bg-white border border-gray-200 text-gray-700 rounded-xl font-medium hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-y-8 gap-x-12">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-500 uppercase tracking-wider mb-1">Full Name</label>
|
||||
<p className="text-base font-semibold text-gray-900">{userData?.name}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-500 uppercase tracking-wider mb-1">Email Address</label>
|
||||
<p className="text-base font-semibold text-gray-900">{userData?.email}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-500 uppercase tracking-wider mb-1">Phone Number</label>
|
||||
<p className="text-base font-semibold text-gray-900">{userData?.phone || 'Not provided'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-500 uppercase tracking-wider mb-1">Role</label>
|
||||
<p className="text-base font-semibold text-gray-900 flex items-center gap-2">
|
||||
<span className="w-2 h-2 rounded-full bg-emerald-500"></span>
|
||||
{userRole}
|
||||
</p>
|
||||
</div>
|
||||
{userData?.department && (
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-500 uppercase tracking-wider mb-1">Department</label>
|
||||
<p className="text-base font-semibold text-gray-900">{userData.department}</p>
|
||||
</div>
|
||||
)}
|
||||
{userData?.year && (
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-500 uppercase tracking-wider mb-1">Year</label>
|
||||
<p className="text-base font-semibold text-gray-900">{userData.year}</p>
|
||||
</div>
|
||||
)}
|
||||
{userData?.section && (
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-500 uppercase tracking-wider mb-1">Section</label>
|
||||
<p className="text-base font-semibold text-gray-900">{userData.section}</p>
|
||||
</div>
|
||||
)}
|
||||
{userData?.reg_no && (
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-500 uppercase tracking-wider mb-1">Registration Number</label>
|
||||
<p className="text-base font-semibold text-gray-900">{userData.reg_no}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Additional Info / Activity Placeholder */}
|
||||
{!isEditing && (
|
||||
<div className="bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden">
|
||||
<div className="px-6 py-5 border-b border-gray-100">
|
||||
<h3 className="text-lg font-bold text-gray-900">Recent Activity</h3>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
{pastEvents.length > 0 ? (
|
||||
<div className="space-y-6">
|
||||
{pastEvents.map((pe, idx) => (
|
||||
<div key={idx} className="flex items-start gap-5 p-4 rounded-2xl hover:bg-gray-50 transition-colors border border-transparent hover:border-gray-100 group">
|
||||
<div className="w-12 h-12 rounded-2xl bg-orange-50 text-orange-500 flex items-center justify-center text-lg shrink-0 shadow-sm border border-orange-100 group-hover:scale-110 transition-transform">
|
||||
<i className="fas fa-calendar-check"></i>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-black text-gray-900 uppercase tracking-tight">{pe.events?.title || 'Unknown Event'}</p>
|
||||
<p className="text-[10px] text-gray-500 mt-1 uppercase font-bold tracking-widest">{pe.events?.category} • Completed on {new Date(pe.participation_date).toLocaleDateString()}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-12 text-gray-400">
|
||||
<div className="w-16 h-16 bg-gray-50 rounded-full flex items-center justify-center mx-auto mb-4 border border-gray-100">
|
||||
<i className="fas fa-history text-2xl opacity-20"></i>
|
||||
</div>
|
||||
<p className="text-xs font-black uppercase tracking-widest text-gray-400">No participation history yet</p>
|
||||
<p className="text-[10px] text-gray-400 mt-2 font-medium">Events you participate in will appear here.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfileView;
|
||||
382
RIT-EVENT-MANAGEMENT--main/components/RegistrationsView.tsx
Normal file
382
RIT-EVENT-MANAGEMENT--main/components/RegistrationsView.tsx
Normal file
@@ -0,0 +1,382 @@
|
||||
import React from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { Event } from '../types';
|
||||
import EventCard from './EventCard';
|
||||
import { supabase } from '../supabase';
|
||||
|
||||
interface RegistrationsViewProps {
|
||||
events: Event[];
|
||||
bookedEventIds: string[];
|
||||
userRegistrations: any[];
|
||||
onToggleBooking: (id: string) => void;
|
||||
onTrackStatus: (event: Event) => void;
|
||||
currentUserName: string;
|
||||
userRole?: string;
|
||||
}
|
||||
|
||||
const Portal: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
return createPortal(children, document.body);
|
||||
};
|
||||
|
||||
const RegistrationsView: React.FC<RegistrationsViewProps> = ({ events, bookedEventIds, userRegistrations, onToggleBooking, onTrackStatus, currentUserName, userRole }) => {
|
||||
const [showInspectModal, setShowInspectModal] = React.useState<{eventId: string, title: string, teamCode: string} | null>(null);
|
||||
const [showRemoveConfirm, setShowRemoveConfirm] = React.useState<{member: any, eventId: string, teamCode: string} | null>(null);
|
||||
const [showRemoveSuccess, setShowRemoveSuccess] = React.useState<{name: string} | null>(null);
|
||||
const [showDisbandConfirm, setShowDisbandConfirm] = React.useState<{eventId: string, teamCode: string} | null>(null);
|
||||
const [showLeaveConfirm, setShowLeaveConfirm] = React.useState<{eventId: string} | null>(null);
|
||||
const [teamMembers, setTeamMembers] = React.useState<any[]>([]);
|
||||
const [isProcessing, setIsProcessing] = React.useState(false);
|
||||
const [copyingCode, setCopyingCode] = React.useState<string | null>(null);
|
||||
|
||||
const bookedEvents = events.filter(e => bookedEventIds.includes(e.id));
|
||||
|
||||
const handleCopyCode = async (code: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(code);
|
||||
setCopyingCode(code);
|
||||
setTimeout(() => setCopyingCode(null), 2000);
|
||||
} catch (err) {
|
||||
console.error("Copy failed", err);
|
||||
}
|
||||
};
|
||||
|
||||
const executeLeaveTeam = async (eventId: string) => {
|
||||
setIsProcessing(true);
|
||||
try {
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
if (!user) return;
|
||||
|
||||
const registrationId = `${user.id}_${eventId}`;
|
||||
const { error } = await supabase.from('registrations').update({
|
||||
team_code: null,
|
||||
team_name: null,
|
||||
is_team_leader: false
|
||||
}).eq('id', registrationId);
|
||||
|
||||
if (error) throw error;
|
||||
setShowLeaveConfirm(null);
|
||||
setShowInspectModal(null);
|
||||
window.location.reload();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert("Failed to leave team");
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const executeDisbandTeam = async (eventId: string, code: string) => {
|
||||
setIsProcessing(true);
|
||||
try {
|
||||
const { error } = await supabase.from('registrations').update({
|
||||
team_code: null,
|
||||
team_name: null,
|
||||
is_team_leader: false
|
||||
}).eq('event_id', eventId).eq('team_code', code);
|
||||
|
||||
if (error) throw error;
|
||||
setShowDisbandConfirm(null);
|
||||
setShowInspectModal(null);
|
||||
window.location.reload();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert("Failed to disband team");
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleInspectTeam = async (eventId: string, code: string, title: string) => {
|
||||
setIsProcessing(true);
|
||||
try {
|
||||
const { data: members, error: fetchErr } = await supabase
|
||||
.from('registrations')
|
||||
.select('user_id, user_name, reg_no, year, dept, section, is_team_leader')
|
||||
.eq('event_id', eventId)
|
||||
.eq('team_code', code);
|
||||
|
||||
if (fetchErr) throw fetchErr;
|
||||
|
||||
const formattedMembers = members.map((m: any) => ({
|
||||
user_id: m.user_id,
|
||||
name: m.user_name,
|
||||
reg_no: m.reg_no,
|
||||
year: m.year,
|
||||
department: m.dept,
|
||||
section: m.section,
|
||||
is_team_leader: m.is_team_leader
|
||||
}));
|
||||
|
||||
setTeamMembers(formattedMembers);
|
||||
setShowInspectModal({ eventId, title, teamCode: code });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert("Failed to fetch team members");
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="pt-40 pb-20 px-12 md:px-24 animate-in fade-in duration-500">
|
||||
<h1 className="text-5xl font-black text-[#1A202C] mb-16 tracking-tight uppercase">My <span className="text-[#f97316]">Registrations</span></h1>
|
||||
|
||||
{bookedEvents.length > 0 ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-10">
|
||||
{bookedEvents.map(event => {
|
||||
const reg = userRegistrations.find(r => String(r.event_id) === String(event.id));
|
||||
return (
|
||||
<div key={event.id} className="space-y-6">
|
||||
<EventCard
|
||||
event={event}
|
||||
isBooked={true}
|
||||
onToggle={() => onToggleBooking(event.id)}
|
||||
onTrackStatus={onTrackStatus}
|
||||
currentUserName={currentUserName}
|
||||
userRole={userRole}
|
||||
registration={reg}
|
||||
/>
|
||||
|
||||
{event.isTeamEvent && (
|
||||
<div className="bg-white rounded-3xl p-6 border border-slate-200 shadow-sm relative overflow-hidden group">
|
||||
<div className="absolute top-0 right-0 w-24 h-24 bg-orange-50/50 rounded-full -translate-y-1/2 translate-x-1/2 blur-2xl group-hover:bg-orange-100/50 transition-colors"></div>
|
||||
{reg?.team_code ? (
|
||||
<div className="flex items-center justify-between relative z-10">
|
||||
<div>
|
||||
<span className="block text-[8px] font-black text-emerald-500 uppercase tracking-widest mb-1 flex items-center gap-1">
|
||||
<i className="fas fa-check-circle"></i> Status: Teamed Up
|
||||
</span>
|
||||
<h4 className="text-sm font-black text-slate-900 uppercase">{reg.team_name}</h4>
|
||||
<p className="text-[10px] text-slate-400 font-bold mt-1 uppercase flex items-center gap-2">
|
||||
CODE: <span className="text-slate-900 underline font-black">{reg.team_code}</span>
|
||||
<button
|
||||
onClick={() => handleCopyCode(reg.team_code)}
|
||||
className="text-slate-400 hover:text-[#f97316] transition-colors"
|
||||
title="Copy Code"
|
||||
>
|
||||
<i className={`fas ${copyingCode === reg.team_code ? 'fa-check text-emerald-500' : 'fa-copy'}`}></i>
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col items-end gap-2">
|
||||
<div className={`w-12 h-12 ${reg.is_team_leader ? 'bg-amber-100 text-amber-600' : 'bg-emerald-50 text-emerald-500'} rounded-2xl flex items-center justify-center text-lg shadow-sm border ${reg.is_team_leader ? 'border-amber-200' : 'border-emerald-100'}`}>
|
||||
<i className={`fas ${reg.is_team_leader ? 'fa-crown animate-pulse' : 'fa-user-group'}`}></i>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleInspectTeam(event.id, reg.team_code, event.title)}
|
||||
className="text-[9px] font-black text-slate-400 uppercase tracking-widest hover:text-[#f97316] transition-colors flex items-center gap-1"
|
||||
>
|
||||
<i className="fas fa-search"></i> Inspect Team
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-5 relative z-10">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-1.5 h-4 bg-slate-200 rounded-full"></div>
|
||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Team Required</span>
|
||||
</div>
|
||||
<p className="text-[9px] text-slate-400 font-bold uppercase leading-relaxed">Please use the "Track Progress" button to manage your team for this event.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center min-h-[40vh] bg-white rounded-[40px] border border-dashed border-gray-300 p-10 text-center">
|
||||
<i className="fas fa-ticket-alt text-6xl text-gray-200 mb-6"></i>
|
||||
<p className="text-gray-400 text-xl font-medium max-w-sm">You haven't booked any events yet, or your previously registered events have been removed.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showInspectModal && (
|
||||
<Portal>
|
||||
<div className="fixed inset-0 z-[10000] flex items-center justify-center p-6 bg-slate-900/60 backdrop-blur-sm animate-in fade-in duration-300">
|
||||
<div className="bg-white rounded-[3rem] w-full max-w-2xl p-10 shadow-2xl animate-in zoom-in-95 duration-500 relative overflow-hidden">
|
||||
<div className="absolute top-0 left-0 w-full h-2 bg-gradient-to-r from-[#f97316] to-amber-500"></div>
|
||||
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<div>
|
||||
<h3 className="text-2xl font-black text-slate-900 uppercase tracking-tight">Team Roster</h3>
|
||||
<p className="text-[10px] text-gray-400 font-bold uppercase tracking-widest mt-1">Code: <span className="text-[#f97316]">{showInspectModal.teamCode}</span> | {showInspectModal.title}</p>
|
||||
</div>
|
||||
<button onClick={() => setShowInspectModal(null)} className="w-10 h-10 rounded-full bg-slate-50 flex items-center justify-center text-slate-400 hover:text-slate-900 transition-colors">
|
||||
<i className="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="max-h-[50vh] overflow-y-auto no-scrollbar space-y-4 pr-2 mb-8">
|
||||
{teamMembers.map((member, idx) => {
|
||||
const isLeader = teamMembers.find(m => m.is_team_leader && m.name === currentUserName);
|
||||
return (
|
||||
<div key={idx} className="bg-slate-50 rounded-3xl p-6 flex items-center justify-between border border-slate-100 hover:border-slate-200 transition-all group">
|
||||
<div className="flex items-center gap-5">
|
||||
<div className="w-14 h-14 bg-white rounded-2xl flex items-center justify-center text-xl text-[#f97316] border border-slate-100 shadow-sm group-hover:scale-110 transition-transform">
|
||||
<i className={`fas ${member.is_team_leader ? 'fa-crown' : 'fa-user'}`}></i>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h4 className="text-sm font-black text-slate-900 uppercase">{member.name}</h4>
|
||||
{member.is_team_leader && <span className="bg-amber-100 text-amber-600 text-[8px] font-black px-2 py-0.5 rounded-full uppercase tracking-tighter">Leader</span>}
|
||||
</div>
|
||||
<p className="text-[10px] text-slate-400 font-bold mt-0.5 uppercase tracking-widest">{member.reg_no}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-6 text-right">
|
||||
<div>
|
||||
<p className="text-[9px] font-black text-slate-900 uppercase">{member.department}</p>
|
||||
<p className="text-[8px] text-slate-400 font-bold uppercase">{member.year} • SEC {member.section}</p>
|
||||
</div>
|
||||
{isLeader && !member.is_team_leader && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowRemoveConfirm({
|
||||
member,
|
||||
eventId: showInspectModal.eventId,
|
||||
teamCode: showInspectModal.teamCode
|
||||
});
|
||||
}}
|
||||
className="w-10 h-10 rounded-xl bg-white text-rose-500 border border-rose-100 flex items-center justify-center transition-all hover:bg-rose-500 hover:text-white shadow-sm"
|
||||
title="Remove from team"
|
||||
>
|
||||
<i className="fas fa-user-minus"></i>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
{teamMembers.find(m => m.is_team_leader && m.name === currentUserName) ? (
|
||||
<button
|
||||
onClick={() => setShowDisbandConfirm({ eventId: showInspectModal.eventId, teamCode: showInspectModal.teamCode })}
|
||||
disabled={isProcessing}
|
||||
className="w-full py-4 bg-rose-50 text-rose-600 border border-rose-100 rounded-[2rem] font-black uppercase text-[10px] tracking-widest hover:bg-rose-100 transition-all flex items-center justify-center gap-2"
|
||||
>
|
||||
<i className="fas fa-trash-can"></i> Disband Team
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => setShowLeaveConfirm({ eventId: showInspectModal?.eventId || '' })}
|
||||
disabled={isProcessing}
|
||||
className="w-full py-4 bg-amber-50 text-amber-600 border border-amber-100 rounded-[2rem] font-black uppercase text-[10px] tracking-widest hover:bg-amber-100 transition-all flex items-center justify-center gap-2"
|
||||
>
|
||||
<i className="fas fa-right-from-bracket"></i> Leave Team
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setShowInspectModal(null)}
|
||||
className="w-full py-5 bg-slate-900 text-white rounded-[2rem] font-black uppercase text-[11px] tracking-[0.2em] shadow-xl hover:bg-black transition-all active:scale-95"
|
||||
>
|
||||
Close Inspection
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Confirmation Modals (Portaled to prevent clipping) */}
|
||||
{showRemoveConfirm && (
|
||||
<Portal>
|
||||
<div className="fixed inset-0 z-[13000] flex items-center justify-center p-6 bg-slate-900/60 backdrop-blur-sm animate-in fade-in duration-300">
|
||||
<div className="bg-white rounded-[2.5rem] w-full max-w-sm overflow-hidden shadow-2xl animate-in zoom-in-95 duration-500">
|
||||
<div className="p-8 text-center">
|
||||
<div className="w-16 h-16 bg-rose-50 text-rose-500 rounded-2xl flex items-center justify-center text-2xl mx-auto mb-6">
|
||||
<i className="fas fa-user-slash"></i>
|
||||
</div>
|
||||
<h3 className="text-xl font-black text-slate-900 uppercase mb-2">Remove Member?</h3>
|
||||
<p className="text-sm text-slate-500 font-medium">Are you sure you want to remove <span className="font-black text-slate-900 uppercase">{showRemoveConfirm.member.name}</span> from the team?</p>
|
||||
</div>
|
||||
<div className="p-6 bg-slate-50 flex gap-4">
|
||||
<button onClick={() => setShowRemoveConfirm(null)} className="flex-1 py-4 bg-white text-slate-400 rounded-2xl font-black uppercase text-[10px] tracking-widest hover:text-slate-900 transition-all border border-slate-200">Cancel</button>
|
||||
<button
|
||||
onClick={async () => {
|
||||
setIsProcessing(true);
|
||||
try {
|
||||
const { error } = await supabase.from('registrations').update({ team_code: null, team_name: null, is_team_leader: false }).eq('event_id', showRemoveConfirm.eventId).eq('user_id', showRemoveConfirm.member.user_id);
|
||||
if (error) throw error;
|
||||
setTeamMembers(prev => prev.filter(m => m.user_id !== showRemoveConfirm.member.user_id));
|
||||
const removedName = showRemoveConfirm.member.name;
|
||||
setShowRemoveConfirm(null);
|
||||
setShowRemoveSuccess({ name: removedName });
|
||||
} catch (err) { console.error(err); alert("Failed to remove member"); } finally { setIsProcessing(false); }
|
||||
}}
|
||||
disabled={isProcessing}
|
||||
className="flex-1 py-4 bg-rose-500 text-white rounded-2xl font-black uppercase text-[10px] tracking-widest hover:bg-rose-600 transition-all shadow-lg shadow-rose-200"
|
||||
>
|
||||
{isProcessing ? 'Removing...' : 'Confirm'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Portal>
|
||||
)}
|
||||
|
||||
{showRemoveSuccess && (
|
||||
<Portal>
|
||||
<div className="fixed inset-0 z-[13000] flex items-center justify-center p-6 bg-slate-900/60 backdrop-blur-sm animate-in fade-in duration-300">
|
||||
<div className="bg-white rounded-[2.5rem] w-full max-w-sm overflow-hidden shadow-2xl animate-in zoom-in-95 duration-500">
|
||||
<div className="p-10 text-center">
|
||||
<div className="w-16 h-16 bg-emerald-50 text-emerald-500 rounded-2xl flex items-center justify-center text-2xl mx-auto mb-6">
|
||||
<i className="fas fa-check-circle"></i>
|
||||
</div>
|
||||
<h3 className="text-xl font-black text-slate-900 uppercase mb-2">Member Removed</h3>
|
||||
<p className="text-sm text-slate-500 font-medium"><span className="font-black text-slate-900 uppercase">{showRemoveSuccess.name}</span> has been removed.</p>
|
||||
<button onClick={() => setShowRemoveSuccess(null)} className="mt-8 w-full py-4 bg-[#1A202C] text-white rounded-2xl font-black uppercase text-[10px] tracking-widest hover:bg-black transition-all shadow-xl shadow-slate-200">Got it</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Portal>
|
||||
)}
|
||||
|
||||
{showLeaveConfirm && (
|
||||
<Portal>
|
||||
<div className="fixed inset-0 z-[13000] flex items-center justify-center p-6 bg-slate-900/60 backdrop-blur-sm animate-in fade-in duration-300">
|
||||
<div className="bg-white rounded-[2.5rem] w-full max-w-sm overflow-hidden shadow-2xl animate-in zoom-in-95 duration-500">
|
||||
<div className="p-8 text-center">
|
||||
<div className="w-16 h-16 bg-rose-50 text-rose-500 rounded-2xl flex items-center justify-center text-2xl mx-auto mb-6">
|
||||
<i className="fas fa-sign-out-alt"></i>
|
||||
</div>
|
||||
<h3 className="text-xl font-black text-slate-900 uppercase mb-2">Leave Team?</h3>
|
||||
<p className="text-sm text-slate-500 font-medium">Are you sure you want to leave this team?</p>
|
||||
</div>
|
||||
<div className="p-6 bg-slate-50 flex gap-4">
|
||||
<button onClick={() => setShowLeaveConfirm(null)} className="flex-1 py-4 bg-white text-slate-400 rounded-2xl font-black uppercase text-[10px] tracking-widest hover:text-slate-900 transition-all border border-slate-200">Cancel</button>
|
||||
<button onClick={() => executeLeaveTeam(showLeaveConfirm.eventId)} disabled={isProcessing} className="flex-1 py-4 bg-rose-500 text-white rounded-2xl font-black uppercase text-[10px] tracking-widest hover:bg-rose-600 transition-all shadow-lg shadow-rose-200">{isProcessing ? 'Leaving...' : 'Confirm'}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Portal>
|
||||
)}
|
||||
|
||||
{showDisbandConfirm && (
|
||||
<Portal>
|
||||
<div className="fixed inset-0 z-[13000] flex items-center justify-center p-6 bg-slate-900/60 backdrop-blur-sm animate-in fade-in duration-300">
|
||||
<div className="bg-white rounded-[2.5rem] w-full max-w-sm overflow-hidden shadow-2xl animate-in zoom-in-95 duration-500">
|
||||
<div className="p-8 text-center">
|
||||
<div className="w-16 h-16 bg-rose-50 text-rose-500 rounded-2xl flex items-center justify-center text-2xl mx-auto mb-6">
|
||||
<i className="fas fa-ban"></i>
|
||||
</div>
|
||||
<h3 className="text-xl font-black text-slate-900 uppercase mb-2">Disband Team?</h3>
|
||||
<p className="text-sm text-slate-500 font-medium">Are you sure? This will disband the team for <span className="font-black text-slate-900">ALL</span> members.</p>
|
||||
</div>
|
||||
<div className="p-6 bg-slate-50 flex gap-4">
|
||||
<button onClick={() => setShowDisbandConfirm(null)} className="flex-1 py-4 bg-white text-slate-400 rounded-2xl font-black uppercase text-[10px] tracking-widest hover:text-slate-900 transition-all border border-slate-200">Cancel</button>
|
||||
<button onClick={() => executeDisbandTeam(showDisbandConfirm.eventId, showDisbandConfirm.teamCode)} disabled={isProcessing} className="flex-1 py-4 bg-rose-500 text-white rounded-2xl font-black uppercase text-[10px] tracking-widest hover:bg-rose-600 transition-all shadow-lg shadow-rose-200">{isProcessing ? 'Disbanding...' : 'Confirm'}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Portal>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Portal>
|
||||
)}
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RegistrationsView;
|
||||
182
RIT-EVENT-MANAGEMENT--main/components/SpecialEventsBanner.tsx
Normal file
182
RIT-EVENT-MANAGEMENT--main/components/SpecialEventsBanner.tsx
Normal file
@@ -0,0 +1,182 @@
|
||||
import React, { useState } from 'react';
|
||||
import { motion, AnimatePresence } from 'motion/react';
|
||||
import { SpecialEvent } from '../types';
|
||||
|
||||
interface SpecialEventsBannerProps {
|
||||
specialEvents: SpecialEvent[];
|
||||
}
|
||||
|
||||
const SpecialEventsBanner: React.FC<SpecialEventsBannerProps> = ({ specialEvents }) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const hasEvents = specialEvents && specialEvents.length > 0;
|
||||
|
||||
const handleRedirect = (link: string) => {
|
||||
window.location.href = link;
|
||||
};
|
||||
|
||||
const defaultTitle = "The Grand Chronicles of RIT";
|
||||
const defaultDescription = "Hark! The tides have brought forth prestigious gatherings and legendary challenges. Seek thy destiny in the links below.";
|
||||
|
||||
return (
|
||||
<section className="py-12 relative overflow-hidden bg-transparent">
|
||||
{/* Background glow (Softer and centered) */}
|
||||
<div className="absolute inset-0 z-0 opacity-5 pointer-events-none">
|
||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[400px] h-[400px] bg-amber-500/20 blur-[80px] rounded-full"></div>
|
||||
</div>
|
||||
|
||||
<div className="max-w-2xl mx-auto px-6 relative z-10">
|
||||
{/* New Header: Descriptive text above the scroll */}
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="font-medieval text-3xl md:text-5xl text-[#3e2723] mb-4 opacity-90">
|
||||
Special Events Registry
|
||||
</h2>
|
||||
<p className="font-parchment text-sm md:text-base text-[#5d4037]/70 italic tracking-wide">
|
||||
Tap the ancient scroll to reveal the current decrees of RIT
|
||||
</p>
|
||||
<div className="w-16 h-[1px] bg-[#8d6e63]/20 mx-auto mt-6"></div>
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial="closed"
|
||||
animate={isOpen ? "open" : "closed"}
|
||||
className="relative flex flex-col items-center"
|
||||
>
|
||||
{/* Top Roller - Interactive */}
|
||||
<motion.div
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="relative z-30 w-full h-16 rounded-full shadow-[0_8px_32px_rgba(62,39,35,0.25)] border-[3px] border-[#3e2723]/60 overflow-hidden cursor-pointer active:scale-95 transition-transform"
|
||||
style={{
|
||||
backgroundImage: 'url(/wood_roller_texture.png)',
|
||||
backgroundSize: '100% 100%',
|
||||
backgroundPosition: 'center'
|
||||
}}
|
||||
>
|
||||
{/* End Caps: Fixes the 'broken corner' look */}
|
||||
<div className="absolute inset-y-0 left-0 w-8 bg-gradient-to-r from-[#1a0f0d] to-transparent opacity-80"></div>
|
||||
<div className="absolute inset-y-0 right-0 w-8 bg-gradient-to-l from-[#1a0f0d] to-transparent opacity-80"></div>
|
||||
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-black/10 via-transparent to-black/20"></div>
|
||||
<div className="absolute inset-x-4 inset-y-0 flex items-center justify-between pointer-events-none px-4">
|
||||
<div className="w-4 h-4 rounded-full border border-white/5 bg-white/5 blur-[1px]"></div>
|
||||
<div className="w-4 h-4 rounded-full border border-white/5 bg-white/5 blur-[1px]"></div>
|
||||
</div>
|
||||
|
||||
<div className="absolute inset-0 flex items-center justify-center opacity-0 hover:opacity-100 transition-opacity">
|
||||
<span className="text-[10px] font-medieval text-white/60 uppercase tracking-[0.3em] bg-black/20 px-4 py-1 rounded-full backdrop-blur-sm">
|
||||
{isOpen ? 'Tap to Close' : 'Tap to Open'}
|
||||
</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Parchment Body */}
|
||||
<motion.div
|
||||
variants={{
|
||||
closed: {
|
||||
height: 0,
|
||||
opacity: 0,
|
||||
transition: { duration: 0.5, ease: "easeInOut" }
|
||||
},
|
||||
open: {
|
||||
height: 'auto',
|
||||
opacity: 1,
|
||||
transition: { duration: 0.8, ease: "easeOut" }
|
||||
}
|
||||
}}
|
||||
className="relative z-20 w-[94%] overflow-hidden origin-top"
|
||||
style={{
|
||||
backgroundImage: 'url(/parchment_texture.png)',
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center'
|
||||
}}
|
||||
>
|
||||
{/* Inner Shadow Shadow */}
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-black/5 via-transparent to-black/5 pointer-events-none shadow-inner"></div>
|
||||
|
||||
<div className="p-8 md:p-12 text-center space-y-8">
|
||||
<motion.div
|
||||
variants={{
|
||||
closed: { opacity: 0, y: -20 },
|
||||
open: { opacity: 1, y: 0 }
|
||||
}}
|
||||
>
|
||||
<h2 className="font-medieval text-3xl md:text-5xl text-[#3e2723] mb-4 drop-shadow-sm">
|
||||
{hasEvents ? defaultTitle : "The Void Chronicles"}
|
||||
</h2>
|
||||
<div className="w-24 h-[1px] bg-[#8d6e63]/20 mx-auto mb-6"></div>
|
||||
<p className="font-parchment text-base md:text-xl text-[#5d4037] leading-relaxed max-w-lg mx-auto italic opacity-90">
|
||||
{hasEvents ? defaultDescription : "The magic portal remains dormant. Check back when the moons align."}
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-6 pt-4">
|
||||
{specialEvents.map((event, index) => (
|
||||
<motion.div
|
||||
key={event.id}
|
||||
variants={{
|
||||
closed: { opacity: 0, scale: 0.95 },
|
||||
open: { opacity: 1, scale: 1 }
|
||||
}}
|
||||
transition={{ delay: isOpen ? 0.3 + (index * 0.1) : 0 }}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
className="group cursor-pointer relative"
|
||||
onClick={() => handleRedirect(event.link)}
|
||||
>
|
||||
<div className="border border-[#8d6e63]/15 bg-[#8d6e63]/5 p-5 rounded-lg backdrop-blur-[1px] transition-all duration-300 group-hover:bg-[#8d6e63]/10 group-hover:border-[#8d6e63]/30">
|
||||
<h3 className="font-medieval text-xl md:text-2xl text-[#3e2723] mb-2 group-hover:text-[#795548] transition-colors">
|
||||
{event.title}
|
||||
</h3>
|
||||
<p className="font-parchment text-xs md:text-sm text-[#5d4037] line-clamp-2 opacity-80">
|
||||
{event.description}
|
||||
</p>
|
||||
<div className="mt-3 flex justify-end">
|
||||
<span className="text-[9px] font-medieval uppercase tracking-[0.2em] text-[#8d6e63] group-hover:text-[#3e2723] flex items-center gap-2">
|
||||
Behold <i className="fas fa-feather-pointed"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{hasEvents && (
|
||||
<div className="font-medieval text-[10px] text-[#8d6e63]/40 uppercase tracking-[0.4em] pt-8">
|
||||
End of Scroll
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Bottom Roller - Interactive */}
|
||||
<motion.div
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
variants={{
|
||||
closed: { y: -64 }, // Perfect overlap (h-16 = 64px)
|
||||
open: { y: 0 }
|
||||
}}
|
||||
transition={{ duration: 0.8, ease: "easeOut" }}
|
||||
className="relative z-30 w-full h-16 rounded-full shadow-[0_-8px_32px_rgba(62,39,35,0.25)] border-[3px] border-[#3e2723]/60 overflow-hidden cursor-pointer active:scale-95 transition-transform"
|
||||
style={{
|
||||
backgroundImage: 'url(/wood_roller_texture.png)',
|
||||
backgroundSize: '100% 100%',
|
||||
backgroundPosition: 'center'
|
||||
}}
|
||||
>
|
||||
{/* End Caps: Fixes the 'broken corner' look */}
|
||||
<div className="absolute inset-y-0 left-0 w-8 bg-gradient-to-r from-[#1a0f0d] to-transparent opacity-80"></div>
|
||||
<div className="absolute inset-y-0 right-0 w-8 bg-gradient-to-l from-[#1a0f0d] to-transparent opacity-80"></div>
|
||||
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/10 via-transparent to-black/20"></div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
<style>{`
|
||||
.font-medieval { font-family: 'Pirata One', cursive; }
|
||||
.font-parchment { font-family: 'Almendra', serif; }
|
||||
`}</style>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default SpecialEventsBanner;
|
||||
|
||||
100
RIT-EVENT-MANAGEMENT--main/components/StatsSection.tsx
Normal file
100
RIT-EVENT-MANAGEMENT--main/components/StatsSection.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { motion } from 'motion/react';
|
||||
import { Event } from '../types';
|
||||
|
||||
interface StatsSectionProps {
|
||||
events: Event[];
|
||||
}
|
||||
|
||||
const StatsSection: React.FC<StatsSectionProps> = ({ events }) => {
|
||||
const [counts, setCounts] = useState({
|
||||
nonTechnical: 0,
|
||||
technical: 0,
|
||||
workshops: 0,
|
||||
totalEvents: 0
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
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;
|
||||
|
||||
setCounts({
|
||||
nonTechnical: nonTech,
|
||||
technical: tech,
|
||||
workshops: workshops,
|
||||
totalEvents: events.length
|
||||
});
|
||||
}
|
||||
}, [events]);
|
||||
|
||||
const stats = [
|
||||
{
|
||||
label: "Active Non-Tech Events",
|
||||
value: counts.nonTechnical,
|
||||
suffix: "+",
|
||||
delay: 0.1
|
||||
},
|
||||
{
|
||||
label: "Technical Events",
|
||||
value: counts.technical,
|
||||
suffix: "+",
|
||||
delay: 0.2
|
||||
},
|
||||
{
|
||||
label: "Workshops",
|
||||
value: counts.workshops,
|
||||
suffix: "+",
|
||||
delay: 0.3
|
||||
},
|
||||
{
|
||||
label: "Total Events",
|
||||
value: counts.totalEvents,
|
||||
suffix: "+",
|
||||
delay: 0.4
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<section className="w-full bg-[#F9FAFB] py-12 px-6 md:px-12 lg:px-20">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{stats.map((stat, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, delay: stat.delay }}
|
||||
viewport={{ once: true }}
|
||||
className="bg-white p-8 rounded-2xl shadow-sm hover:shadow-md transition-shadow duration-300 border border-gray-100 relative overflow-hidden group"
|
||||
>
|
||||
{/* Corner accents similar to the image */}
|
||||
<div className="absolute top-2 right-2 opacity-50 transform rotate-90">
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1 19V5C1 2.79086 2.79086 1 5 1H19" stroke="#f97316" strokeWidth="2" strokeLinecap="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="absolute bottom-2 left-2 opacity-50 transform -rotate-90">
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1 19V5C1 2.79086 2.79086 1 5 1H19" stroke="#f97316" strokeWidth="2" strokeLinecap="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center justify-center text-center z-10 relative">
|
||||
<h3 className="text-4xl md:text-5xl font-bold text-[#2D3748] mb-2 font-serif group-hover:text-[#f97316] transition-colors duration-300">
|
||||
{stat.value}{stat.suffix}
|
||||
</h3>
|
||||
<p className="text-gray-500 font-medium text-sm uppercase tracking-wider">
|
||||
{stat.label}
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatsSection;
|
||||
591
RIT-EVENT-MANAGEMENT--main/components/StatusTrackerView.tsx
Normal file
591
RIT-EVENT-MANAGEMENT--main/components/StatusTrackerView.tsx
Normal file
@@ -0,0 +1,591 @@
|
||||
import React, { useState, useMemo, useEffect } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { Event } from '../types';
|
||||
import { uploadToSupabase, supabase } from '../supabase';
|
||||
|
||||
|
||||
interface StatusTrackerViewProps {
|
||||
event: Event;
|
||||
registration?: any;
|
||||
onBack: () => void;
|
||||
onUploadCertificate: (data: string) => Promise<string>;
|
||||
onShowCreateTeam?: () => void;
|
||||
onShowJoinTeam?: () => void;
|
||||
}
|
||||
|
||||
const SuccessPopup: React.FC<{ onClose: () => void }> = ({ onClose }) => (
|
||||
<div className="fixed inset-0 z-[10005] flex items-center justify-center p-6 bg-black/80 backdrop-blur-xl animate-in fade-in duration-300">
|
||||
<div className="bg-white rounded-[3rem] w-full max-w-sm p-12 shadow-2xl flex flex-col items-center text-center animate-in zoom-in-95 duration-500">
|
||||
<div className="w-24 h-24 bg-emerald-100 text-emerald-500 rounded-full flex items-center justify-center text-4xl mb-8 shadow-inner">
|
||||
<i className="fas fa-cloud-check"></i>
|
||||
</div>
|
||||
<h3 className="text-2xl font-black text-slate-900 mb-2 uppercase tracking-tight">File Captured</h3>
|
||||
<p className="text-gray-500 font-bold text-[10px] uppercase tracking-[0.3em] mb-10">Proof uploaded successfully</p>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="w-full py-5 bg-emerald-600 text-white rounded-2xl font-black uppercase tracking-widest text-[11px] hover:bg-emerald-700 transition-all shadow-xl shadow-emerald-200 active:scale-95"
|
||||
>
|
||||
Track Verification
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
|
||||
const StatusTrackerView: React.FC<StatusTrackerViewProps> = ({ event, registration, onBack, onUploadCertificate, onShowCreateTeam, onShowJoinTeam }) => {
|
||||
const fileInputRef = React.useRef<HTMLInputElement>(null);
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const [showSuccess, setShowSuccess] = useState(false);
|
||||
const [now, setNow] = useState(new Date());
|
||||
const [attendanceRecords, setAttendanceRecords] = useState<any[]>([]);
|
||||
|
||||
const fetchAttendance = async () => {
|
||||
if (!registration?.id) return;
|
||||
const { data, error } = await supabase
|
||||
.from('attendance_records')
|
||||
.select('*')
|
||||
.eq('registration_id', registration.id);
|
||||
if (!error && data) {
|
||||
setAttendanceRecords(data);
|
||||
}
|
||||
};
|
||||
|
||||
const odUrl = registration?.od_url || registration?.od;
|
||||
const certUrl = registration?.certification_url || registration?.certifications;
|
||||
const certStatus = registration?.certification_status || registration?.certification_approval;
|
||||
|
||||
const [liveOdUrl, setLiveOdUrl] = useState(odUrl);
|
||||
const [liveCertUrl, setLiveCertUrl] = useState(certUrl);
|
||||
const [liveCertStatus, setLiveCertStatus] = useState(certStatus);
|
||||
|
||||
useEffect(() => {
|
||||
// Dynamic refetch to catch real-time faculty OD uploads without full page reload
|
||||
if (registration?.id) {
|
||||
supabase.from('registrations').select('*').eq('id', registration.id).single().then(({ data }) => {
|
||||
if (data) {
|
||||
setLiveOdUrl(data.od_url || data.od);
|
||||
setLiveCertUrl(data.certification_url || data.certifications);
|
||||
setLiveCertStatus(data.certification_status || data.certification_approval);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (registration?.id) {
|
||||
fetchAttendance();
|
||||
|
||||
// Real-time subscription for attendance
|
||||
const channel = supabase
|
||||
.channel(`attendance-${registration.id}`)
|
||||
.on(
|
||||
'postgres_changes',
|
||||
{
|
||||
event: '*',
|
||||
schema: 'public',
|
||||
table: 'attendance_records',
|
||||
filter: `registration_id=eq.${registration.id}`
|
||||
},
|
||||
() => {
|
||||
fetchAttendance();
|
||||
}
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
return () => {
|
||||
supabase.removeChannel(channel);
|
||||
};
|
||||
}
|
||||
const timer = setInterval(() => setNow(new Date()), 30000);
|
||||
return () => clearInterval(timer);
|
||||
}, [registration?.id, event.id]);
|
||||
|
||||
const isEventEnded = event.status === 'Completed';
|
||||
|
||||
const progressSteps = useMemo(() => {
|
||||
const isFree = event.pricingType === 'FREE';
|
||||
const isPaidVerified = registration?.payment_status === 'COMPLETED';
|
||||
const isManuallyEnded = event.status === 'Completed';
|
||||
const isManuallyOngoing = event.status === 'Event Ongoing';
|
||||
|
||||
const isEnded = isManuallyEnded;
|
||||
const isOngoing = isManuallyOngoing;
|
||||
|
||||
const hasUploaded = !!liveCertUrl;
|
||||
const isApproved = liveCertStatus === 'APPROVED';
|
||||
const hasUploadedOd = !!liveOdUrl;
|
||||
|
||||
return [
|
||||
{
|
||||
label: 'Registered',
|
||||
status: 'completed',
|
||||
icon: 'fa-user-check',
|
||||
color: 'bg-emerald-500',
|
||||
detail: 'Identity Secured'
|
||||
},
|
||||
{
|
||||
label: 'TEAM',
|
||||
status: !event.isTeamEvent || registration?.team_code ? 'completed' : 'active',
|
||||
icon: 'fa-users',
|
||||
color: 'bg-orange-500',
|
||||
detail: !event.isTeamEvent ? 'Solo Mode' : (registration?.team_code ? `Team: ${registration.team_name || 'Joined'}` : 'Wait for Team')
|
||||
},
|
||||
{
|
||||
label: 'Payment',
|
||||
status: isFree || isPaidVerified ? 'completed' : 'active',
|
||||
icon: 'fa-credit-card',
|
||||
color: 'bg-blue-400',
|
||||
detail: isFree ? 'Waiver Applied' : (isPaidVerified ? 'Funds Verified' : 'Awaiting Payment')
|
||||
},
|
||||
{
|
||||
label: 'Ticket',
|
||||
status: isFree || isPaidVerified ? 'completed' : 'pending',
|
||||
icon: 'fa-ticket-alt',
|
||||
color: 'bg-amber-500',
|
||||
detail: isFree || isPaidVerified ? 'Access Granted' : 'Locked'
|
||||
},
|
||||
{
|
||||
label: 'Ongoing',
|
||||
status: isEnded ? 'completed' : (isOngoing ? 'active' : 'pending'),
|
||||
icon: 'fa-play-circle',
|
||||
color: 'bg-purple-500',
|
||||
detail: isOngoing ? 'Live Session' : (isEnded ? 'Session Ended' : 'Scheduled')
|
||||
},
|
||||
{
|
||||
label: 'Ended',
|
||||
status: isEnded ? 'completed' : 'pending',
|
||||
icon: 'fa-calendar-check',
|
||||
color: 'bg-rose-500',
|
||||
detail: isEnded ? 'Archived' : 'Wait for Admin'
|
||||
},
|
||||
{
|
||||
label: 'Certification',
|
||||
status: isApproved ? 'completed' : (isEnded ? 'active' : 'pending'),
|
||||
icon: isApproved ? 'fa-check-double' : (hasUploaded ? 'fa-spinner fa-spin' : 'fa-award'),
|
||||
color: isApproved ? 'bg-emerald-600' : (isEnded ? 'bg-amber-500' : 'bg-indigo-600'),
|
||||
detail: isApproved ? 'Verified by Faculty' : (hasUploaded ? 'In Review' : (isEnded ? 'Upload Proof' : 'Wait for End'))
|
||||
},
|
||||
{
|
||||
label: 'OD',
|
||||
status: hasUploadedOd ? 'completed' : (isApproved ? 'active' : 'pending'),
|
||||
icon: 'fa-file-signature',
|
||||
color: 'bg-teal-500',
|
||||
detail: hasUploadedOd ? 'OD Provided' : (isApproved ? 'Ready for Download' : 'Wait for Approval')
|
||||
},
|
||||
];
|
||||
}, [event, registration, now, liveOdUrl, liveCertUrl, liveCertStatus]);
|
||||
|
||||
const handleOdDownload = async () => {
|
||||
if (!liveOdUrl) return;
|
||||
try {
|
||||
const response = await fetch(liveOdUrl);
|
||||
const blob = await response.blob();
|
||||
|
||||
const isPdf = liveOdUrl.toLowerCase().endsWith('.pdf') || blob.type === 'application/pdf';
|
||||
const extension = isPdf ? 'pdf' : 'jpg';
|
||||
|
||||
const blobUrl = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = blobUrl;
|
||||
link.download = `OD_${event.title.replace(/\s+/g, '_')}.${extension}`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(blobUrl);
|
||||
} catch (err) {
|
||||
console.error("OD Download failed:", err);
|
||||
// Fallback: just open the URL directly if local fetch somehow fails
|
||||
window.open(liveOdUrl, '_blank');
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
|
||||
if (file && user && registration) {
|
||||
setIsUploading(true);
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = async () => {
|
||||
try {
|
||||
const base64 = reader.result as string;
|
||||
const fileName = `Cert_${user.id}_${event.id}_${Date.now()}.jpg`;
|
||||
|
||||
// STORE IN 'Certifications' BUCKET AS REQUESTED
|
||||
const publicUrl = await uploadToSupabase(base64, fileName, 'Certifications');
|
||||
|
||||
await supabase.from('registrations').update({
|
||||
certification_url: publicUrl,
|
||||
certification_status: 'PENDING_APPROVAL'
|
||||
}).eq('id', registration.id);
|
||||
|
||||
setShowSuccess(true);
|
||||
} catch (err) {
|
||||
console.error("Upload failed:", err);
|
||||
alert("Upload failed. Please check your connection or bucket permissions.");
|
||||
} finally {
|
||||
setIsUploading(false);
|
||||
}
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="pt-40 pb-16 px-6 md:px-12 lg:px-24 bg-[#F3F4F6] min-h-screen animate-in fade-in duration-500 font-inter">
|
||||
{showSuccess && <SuccessPopup onClose={() => { setShowSuccess(false); window.location.reload(); }} />}
|
||||
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6 mb-16">
|
||||
<div className="flex flex-col items-start">
|
||||
<button onClick={onBack} className="flex items-center gap-2 text-[#f97316] font-black uppercase tracking-[0.2em] mb-6 text-[10px] group">
|
||||
<i className="fas fa-arrow-left transition-transform group-hover:-translate-x-1"></i> Return to Hub
|
||||
</button>
|
||||
<h1 className="text-6xl font-black text-[#1A202C] tracking-tighter mb-2 leading-none uppercase">REAL-TIME <span className="text-[#f97316]">PROGRESS</span></h1>
|
||||
<p className="text-gray-400 font-bold uppercase tracking-[0.3em] text-xs">Tracking Node: {event.title}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-[3.5rem] p-8 md:p-16 shadow-xl shadow-gray-900/5 border border-gray-100 mb-12 relative overflow-hidden">
|
||||
<div className="relative z-10 w-full overflow-x-auto no-scrollbar pb-6">
|
||||
<div className="min-w-[800px] relative">
|
||||
|
||||
{/* Continuous Line Background */}
|
||||
<div
|
||||
className="absolute top-2.5 h-1.5 bg-gray-100 rounded-full z-0"
|
||||
style={{ left: `${100 / (progressSteps.length * 2)}%`, right: `${100 / (progressSteps.length * 2)}%` }}
|
||||
></div>
|
||||
|
||||
{/* Active Line Foreground */}
|
||||
<div
|
||||
className="absolute top-2.5 h-1.5 bg-gradient-to-r from-emerald-400 to-orange-400 rounded-full z-0 transition-all duration-1000"
|
||||
style={{
|
||||
left: `${100 / (progressSteps.length * 2)}%`,
|
||||
width: `calc(${
|
||||
(progressSteps.findIndex(s => s.status === 'active') !== -1
|
||||
? progressSteps.findIndex(s => s.status === 'active')
|
||||
: Math.max(0, progressSteps.filter(s => s.status === 'completed').length - 1))
|
||||
/ (progressSteps.length - 1)
|
||||
} * (100% - ${100 / progressSteps.length}%))`
|
||||
}}
|
||||
></div>
|
||||
|
||||
{/* Steps Container */}
|
||||
<div className="flex w-full justify-between items-start relative z-10">
|
||||
{progressSteps.map((step, idx) => (
|
||||
<div key={idx} className="flex flex-col items-center flex-1 relative group cursor-pointer">
|
||||
|
||||
{/* Dot */}
|
||||
<div className={`w-6 h-6 rounded-full border-4 border-white shadow-sm mb-10 transition-all duration-500 group-hover:scale-125 ${
|
||||
step.status === 'completed' ? 'bg-emerald-500' :
|
||||
step.status === 'active' ? 'bg-orange-500 ring-4 ring-orange-100' :
|
||||
'bg-gray-200'
|
||||
}`}></div>
|
||||
|
||||
{/* Icon */}
|
||||
<div className={`w-16 h-16 md:w-20 md:h-20 rounded-full flex items-center justify-center transition-all duration-500 mb-6 group-hover:-translate-y-2 group-hover:shadow-lg ${
|
||||
step.status === 'completed' ? 'bg-emerald-50 text-emerald-500 group-hover:bg-emerald-100' :
|
||||
step.status === 'active' ? 'bg-white border-2 border-orange-500 text-orange-500 shadow-[0_0_20px_rgba(249,115,22,0.2)] scale-110 group-hover:scale-125' :
|
||||
'bg-gray-50 text-gray-300 group-hover:bg-gray-100 group-hover:text-gray-500'
|
||||
}`}>
|
||||
<i className={`fas ${step.icon} text-xl md:text-2xl transition-transform duration-300 group-hover:scale-110`}></i>
|
||||
</div>
|
||||
|
||||
{/* Text */}
|
||||
<div className="text-center transition-transform duration-300 group-hover:translate-y-1">
|
||||
<span className={`block text-[10px] md:text-xs font-black uppercase tracking-[0.15em] mb-1.5 transition-colors duration-300 ${
|
||||
step.status === 'active' ? 'text-orange-500' :
|
||||
step.status === 'completed' ? 'text-gray-700 group-hover:text-emerald-600' :
|
||||
'text-gray-400 group-hover:text-gray-600'
|
||||
}`}>{step.label}</span>
|
||||
<p className="text-[9px] font-bold text-gray-400 uppercase tracking-widest whitespace-nowrap">{step.detail}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Attendance Tracker Redesign (Node-based) */}
|
||||
<div className="mt-20 pt-12 border-t border-gray-100 flex flex-col items-center">
|
||||
<div className="flex items-center gap-3 mb-10">
|
||||
<div className="w-1.5 h-4 bg-[#f97316] rounded-full"></div>
|
||||
<span className="text-[11px] font-black text-slate-800 uppercase tracking-[0.2em]">Attendance Milestones</span>
|
||||
</div>
|
||||
|
||||
<div className="w-full max-w-4xl relative">
|
||||
{(() => {
|
||||
const allSessions: { label: string, status: string, day: string, batch: string }[] = [];
|
||||
if (event.schedule && event.schedule.length > 0) {
|
||||
// Group by day_idx
|
||||
const dayMap = new Map<number, typeof event.schedule>();
|
||||
event.schedule.forEach(s => {
|
||||
if (!dayMap.has(s.day_idx)) dayMap.set(s.day_idx, []);
|
||||
dayMap.get(s.day_idx)!.push(s);
|
||||
});
|
||||
const sortedDays = Array.from(dayMap.entries()).sort((a, b) => a[0] - b[0]);
|
||||
sortedDays.forEach(([dayIdx, slots]) => {
|
||||
slots!.forEach((slot) => {
|
||||
const dLabel = `Day ${dayIdx}`;
|
||||
const bLabel = `Batch ${slot.batch_idx}`;
|
||||
const record = attendanceRecords.find(r => r.day_label === dLabel && r.batch_label === bLabel);
|
||||
allSessions.push({
|
||||
label: `D${dayIdx} B${slot.batch_idx}`,
|
||||
day: dLabel,
|
||||
batch: bLabel,
|
||||
status: record?.is_present ? 'completed' : (record?.is_present === false ? 'absent' : 'pending')
|
||||
});
|
||||
});
|
||||
});
|
||||
} else {
|
||||
const record = attendanceRecords.find(r => r.day_label === 'Day 1');
|
||||
allSessions.push({
|
||||
label: 'Day 1',
|
||||
day: 'Day 1',
|
||||
batch: '',
|
||||
status: record?.is_present ? 'completed' : (record?.is_present === false ? 'absent' : 'pending')
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
{/* Connecting Line Background */}
|
||||
<div
|
||||
className="absolute top-2.5 h-1.5 bg-gray-100 rounded-full z-0"
|
||||
style={{ left: `${100 / (allSessions.length * 2)}%`, right: `${100 / (allSessions.length * 2)}%` }}
|
||||
></div>
|
||||
|
||||
{/* Connecting Line Foreground */}
|
||||
<div
|
||||
className="absolute top-2.5 h-1.5 bg-emerald-500 rounded-full z-0 transition-all duration-1000"
|
||||
style={{
|
||||
left: `${100 / (allSessions.length * 2)}%`,
|
||||
width: `calc(${
|
||||
(allSessions.findIndex(s => s.status === 'pending') !== -1
|
||||
? Math.max(0, allSessions.findIndex(s => s.status === 'pending') - 1)
|
||||
: Math.max(0, allSessions.filter(s => s.status === 'completed').length - 1))
|
||||
/ Math.max(1, allSessions.length - 1)
|
||||
} * (100% - ${100 / allSessions.length}%))`
|
||||
}}
|
||||
></div>
|
||||
|
||||
{/* Nodes */}
|
||||
<div className="flex w-full justify-between items-start relative z-10">
|
||||
{allSessions.map((session, sIdx) => (
|
||||
<div key={sIdx} className="flex flex-col items-center flex-1 relative group cursor-pointer">
|
||||
|
||||
{/* Dot on line */}
|
||||
<div className={`w-6 h-6 rounded-full border-4 border-white shadow-sm mb-10 transition-all duration-500 group-hover:scale-125 ${
|
||||
session.status === 'completed' ? 'bg-emerald-500' :
|
||||
session.status === 'absent' ? 'bg-rose-500 animate-pulse' :
|
||||
'bg-gray-200'
|
||||
}`}></div>
|
||||
|
||||
{/* Large Circle Shaped Indicator */}
|
||||
<div className={`w-16 h-16 md:w-20 md:h-20 rounded-full flex items-center justify-center transition-all duration-500 mb-6 group-hover:-translate-y-2 group-hover:shadow-lg ${
|
||||
session.status === 'completed' ? 'bg-emerald-50 text-emerald-500 group-hover:bg-emerald-100 border-2 border-emerald-500/20' :
|
||||
session.status === 'absent' ? 'bg-rose-50 text-rose-500 group-hover:bg-rose-100 border-2 border-rose-500/20' :
|
||||
'bg-gray-50 text-gray-300 group-hover:bg-gray-100 group-hover:text-gray-500 border-2 border-slate-100'
|
||||
}`}>
|
||||
<i className={`fas ${
|
||||
session.status === 'completed' ? 'fa-calendar-check' :
|
||||
session.status === 'absent' ? 'fa-calendar-times' :
|
||||
'fa-calendar'
|
||||
} text-xl md:text-2xl transition-transform duration-300 group-hover:scale-110`}></i>
|
||||
</div>
|
||||
|
||||
{/* Text */}
|
||||
<div className="text-center transition-transform duration-300 group-hover:translate-y-1">
|
||||
<span className={`block text-[10px] md:text-xs font-black uppercase tracking-[0.15em] mb-1.5 transition-colors duration-300 ${
|
||||
session.status === 'completed' ? 'text-[#1A202C]' :
|
||||
session.status === 'absent' ? 'text-rose-500' :
|
||||
'text-gray-400 group-hover:text-gray-600'
|
||||
}`}>{session.label}</span>
|
||||
<p className="text-[9px] font-bold text-gray-400 uppercase tracking-widest whitespace-nowrap">
|
||||
{session.status === 'completed' ? 'Attended' :
|
||||
session.status === 'absent' ? 'Absent' : 'Upcoming'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
|
||||
<div className="mt-12 bg-gray-50/50 px-8 py-3 rounded-2xl border border-gray-100">
|
||||
<p className="text-[10px] font-black text-gray-500 uppercase tracking-[0.2em] flex items-center gap-3">
|
||||
<i className="fas fa-chart-line text-[#f97316]"></i>
|
||||
{attendanceRecords.filter(r => r.is_present).length} / {Math.max(1, event.schedule?.length || 1)} Sessions Attended
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Team Management Buttons - Relocated from RegistrationsView */}
|
||||
{event.isTeamEvent && !registration?.team_code && (
|
||||
<div className="mt-12 p-10 bg-white rounded-[3.5rem] shadow-xl shadow-gray-900/5 border border-gray-100 relative overflow-hidden group animate-in slide-in-from-bottom-5">
|
||||
<div className="absolute top-0 right-0 w-32 h-32 bg-orange-50 rounded-full -translate-y-1/2 translate-x-1/2 blur-2xl group-hover:bg-orange-100 transition-colors"></div>
|
||||
|
||||
<div className="flex flex-col md:flex-row items-center justify-between gap-8 relative z-10">
|
||||
<div className="text-center md:text-left">
|
||||
<div className="flex justify-center md:justify-start items-center gap-3 mb-3">
|
||||
<div className="w-2 h-5 bg-[#f97316] rounded-full"></div>
|
||||
<span className="text-xs font-black text-slate-900 uppercase tracking-widest">Team Management</span>
|
||||
</div>
|
||||
<p className="text-xs text-slate-400 font-bold uppercase tracking-widest max-w-md">This is a team event. Form your own team or join an existing alliance to participate in this competition.</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 w-full md:w-auto">
|
||||
<button
|
||||
onClick={onShowCreateTeam}
|
||||
className="flex-1 md:flex-none py-5 px-10 bg-white border-2 border-slate-100 text-slate-900 rounded-2xl font-black uppercase text-[10px] tracking-[0.2em] shadow-sm hover:border-[#f97316] hover:text-[#f97316] transition-all active:scale-95 flex items-center justify-center gap-2"
|
||||
>
|
||||
<i className="fas fa-plus-circle"></i> Create Team
|
||||
</button>
|
||||
<button
|
||||
onClick={onShowJoinTeam}
|
||||
className="flex-1 md:flex-none py-5 px-10 bg-slate-900 text-white rounded-2xl font-black uppercase text-[10px] tracking-[0.2em] shadow-xl shadow-gray-200 hover:bg-black transition-all active:scale-95 flex items-center justify-center gap-2"
|
||||
>
|
||||
<i className="fas fa-right-to-bracket"></i> Join Alliance
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* OD Document Download Section */}
|
||||
{liveOdUrl && (
|
||||
<div className="bg-white rounded-[3.5rem] p-8 md:p-12 shadow-xl shadow-gray-900/5 border border-gray-100 mb-12 flex flex-col md:flex-row items-center justify-between gap-8 animate-in slide-in-from-bottom-5">
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="w-16 h-16 bg-teal-50 text-teal-500 rounded-3xl flex items-center justify-center text-2xl shadow-inner">
|
||||
<i className="fas fa-file-signature"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-2xl font-black text-[#1A202C] tracking-tight uppercase">Official On-Duty Document</h3>
|
||||
<p className="text-[10px] font-black text-gray-400 uppercase tracking-widest mt-1">Authorized by Faculty Coordinator</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-4 w-full md:w-auto">
|
||||
<a href={liveOdUrl} target="_blank" rel="noreferrer" className="flex-1 md:w-auto px-8 py-4 bg-gray-50 text-gray-600 rounded-2xl font-black text-[10px] uppercase tracking-widest hover:bg-gray-100 transition-all border border-gray-200 text-center">
|
||||
<i className="fas fa-eye mr-2"></i> Inspect
|
||||
</a>
|
||||
<button onClick={handleOdDownload} className="flex-1 md:w-auto px-8 py-4 bg-teal-600 text-white rounded-2xl font-black text-[10px] uppercase tracking-widest hover:bg-teal-700 transition-all shadow-xl shadow-teal-600/20 text-center">
|
||||
<i className="fas fa-download mr-2"></i> Download OD
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
<div className="lg:col-span-2 bg-white p-12 rounded-[3.5rem] shadow-xl shadow-gray-900/10 border border-gray-200 flex flex-col md:flex-row items-center gap-12">
|
||||
<div className="flex-1 text-center md:text-left">
|
||||
<h3 className="text-3xl font-black text-[#1A202C] mb-4 uppercase tracking-tight">Certification Portal</h3>
|
||||
<p className="text-gray-500 font-medium mb-8 leading-relaxed max-w-sm">Provide valid proof of attendance or task completion to finalize your official event participation.</p>
|
||||
|
||||
<input type="file" ref={fileInputRef} className="hidden" onChange={handleFileChange} accept="image/*" />
|
||||
|
||||
{!isEventEnded ? (
|
||||
<div className="flex flex-col items-start gap-3">
|
||||
<button
|
||||
disabled
|
||||
className="px-12 py-5 rounded-2xl font-black text-xs uppercase tracking-[0.3em] bg-gray-100 text-gray-400 cursor-not-allowed border border-gray-200 flex items-center gap-4"
|
||||
>
|
||||
<i className="fas fa-lock"></i> UPLOAD LOCKED
|
||||
</button>
|
||||
<p className="text-[10px] font-black text-gray-400 uppercase tracking-widest flex items-center gap-2">
|
||||
<i className="fas fa-hourglass-half text-[#f97316]"></i>
|
||||
Upload unlocks once admin marks event as Ended
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={isUploading || liveCertStatus === 'APPROVED'}
|
||||
className={`px-12 py-5 rounded-2xl font-black text-xs uppercase tracking-[0.3em] shadow-2xl transition-all active:scale-95 flex items-center gap-4 ${liveCertStatus === 'APPROVED' ? 'bg-emerald-50 text-emerald-500 cursor-not-allowed border border-emerald-100' :
|
||||
isUploading ? 'bg-gray-200 text-gray-400 cursor-wait' : 'bg-[#1A202C] text-white hover:bg-black'
|
||||
}`}
|
||||
>
|
||||
{isUploading ? <><i className="fas fa-spinner fa-spin"></i> SYNCING...</> :
|
||||
liveCertStatus === 'APPROVED' ? <><i className="fas fa-check-double"></i> VERIFIED</> :
|
||||
liveCertUrl ? <><i className="fas fa-clock"></i> RE-UPLOAD PROOF</> :
|
||||
<><i className="fas fa-cloud-arrow-up"></i> UPLOAD PROOF</>}
|
||||
</button>
|
||||
{liveCertStatus === 'PENDING_APPROVAL' && (
|
||||
<p className="mt-4 text-[10px] font-black text-[#f97316] uppercase tracking-widest flex items-center gap-2">
|
||||
<i className="fas fa-info-circle"></i> Awaiting Faculty Review
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="w-full md:w-64 aspect-square bg-gray-50 rounded-[2.5rem] border-2 border-dashed border-gray-200 flex items-center justify-center overflow-hidden group">
|
||||
{liveCertUrl ? (
|
||||
<img src={liveCertUrl} className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-500" alt="Proof" />
|
||||
) : (
|
||||
<div className="text-center p-6">
|
||||
<i className="fas fa-image text-gray-200 text-4xl mb-4"></i>
|
||||
<p className="text-[10px] font-black text-gray-300 uppercase tracking-widest">Preview Area</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-[#1A202C] p-12 rounded-[3.5rem] text-white shadow-2xl shadow-gray-900/20 relative overflow-hidden">
|
||||
<div className="absolute top-0 right-0 w-32 h-32 bg-white/5 rounded-full -translate-y-1/2 translate-x-1/2 blur-2xl"></div>
|
||||
<h3 className="text-2xl font-black mb-10 tracking-tight uppercase flex items-center gap-4">
|
||||
Event Info
|
||||
<span className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></span>
|
||||
</h3>
|
||||
<div className="space-y-8">
|
||||
<div className="flex justify-between items-center border-b border-white/5 pb-4">
|
||||
<span className="text-white/40 text-[10px] font-black uppercase tracking-widest">Conducting Club</span>
|
||||
<span className="text-white font-black text-[11px] uppercase">{event.club || 'N/A'}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center border-b border-white/5 pb-4">
|
||||
<span className="text-white/40 text-[10px] font-black uppercase tracking-widest">Enrollment Status</span>
|
||||
<span className="text-white font-black text-[11px] uppercase">{registration?.payment_status || 'PENDING'}</span>
|
||||
</div>
|
||||
{event.durationDays && (
|
||||
<div className="flex justify-between items-center border-b border-white/5 pb-4">
|
||||
<span className="text-white/40 text-[10px] font-black uppercase tracking-widest">Duration</span>
|
||||
<span className="text-white font-black text-[11px] uppercase">{event.durationDays} Days</span>
|
||||
</div>
|
||||
)}
|
||||
{event.schedule && event.schedule.length > 0 && (
|
||||
<div className="flex justify-between items-center border-b border-white/5 pb-4">
|
||||
<span className="text-white/40 text-[10px] font-black uppercase tracking-widest">Sessions</span>
|
||||
<span className="text-white font-black text-[11px] uppercase">
|
||||
{event.schedule.length} Total
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-between items-center border-b border-white/5 pb-4">
|
||||
<span className="text-white/40 text-[10px] font-black uppercase tracking-widest">Attendance Progress</span>
|
||||
<span className="text-emerald-400 font-black text-[11px] uppercase">
|
||||
{attendanceRecords.filter(r => r.is_present).length} Marked
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center border-b border-white/5 pb-4">
|
||||
<span className="text-white/40 text-[10px] font-black uppercase tracking-widest">Certification</span>
|
||||
<span className={`font-black text-[11px] uppercase ${liveCertStatus === 'APPROVED' ? 'text-emerald-400' : 'text-[#f97316]'}`}>
|
||||
{liveCertStatus?.replace('_', ' ') || 'NOT SUBMITTED'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-white/40 text-[10px] font-black uppercase tracking-widest">Verification Status</span>
|
||||
<span className={`font-black text-[11px] uppercase ${liveCertStatus === 'APPROVED' ? 'text-emerald-400' : 'text-white/60'}`}>
|
||||
{liveCertStatus === 'APPROVED' ? 'Finalized' : 'Pending'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatusTrackerView;
|
||||
146
RIT-EVENT-MANAGEMENT--main/components/TicketVerificationView.tsx
Normal file
146
RIT-EVENT-MANAGEMENT--main/components/TicketVerificationView.tsx
Normal file
@@ -0,0 +1,146 @@
|
||||
import React from 'react';
|
||||
import { Ticket, Event } from '../types';
|
||||
|
||||
interface TicketVerificationViewProps {
|
||||
ticket: Ticket;
|
||||
event: Event;
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
const TicketVerificationView: React.FC<TicketVerificationViewProps> = ({ ticket, event, onBack }) => {
|
||||
return (
|
||||
<div className="min-h-screen bg-black flex flex-col items-center justify-center p-6 md:p-12 animate-in fade-in duration-700">
|
||||
{/* Background Decorative Mesh */}
|
||||
<div className="fixed inset-0 pointer-events-none opacity-20">
|
||||
<div className="absolute top-0 right-0 w-[600px] h-[600px] bg-amber-500 rounded-full blur-[150px] -translate-y-1/2 translate-x-1/2"></div>
|
||||
<div className="absolute bottom-0 left-0 w-[600px] h-[600px] bg-[#f97316] rounded-full blur-[150px] translate-y-1/2 -translate-x-1/2"></div>
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 w-full max-w-5xl">
|
||||
<div className="flex justify-between items-center mb-10 w-full">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-[3px] h-12 bg-[#f97316] rounded-full"></div>
|
||||
<div className="flex flex-col leading-none tracking-tight">
|
||||
<span className="text-xl font-black text-white uppercase">RAJALAKSHMI</span>
|
||||
<span className="text-[10px] font-bold text-white/50 tracking-[0.2em] uppercase py-1">INSTITUTE OF TECHNOLOGY</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={onBack}
|
||||
className="px-8 py-3 bg-[#f97316] text-white rounded-full font-black text-[10px] uppercase tracking-widest hover:bg-[#ea580c] transition-all shadow-xl shadow-orange-950/20 active:scale-95"
|
||||
>
|
||||
RETURN TO PORTAL
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* GOLDEN TICKET DESIGN - Matching Screenshot Concept */}
|
||||
<div className="relative w-full rounded-[2.5rem] overflow-hidden shadow-[0_50px_100px_rgba(0,0,0,0.5)] flex flex-col md:flex-row border border-white/10 group">
|
||||
|
||||
{/* Main Gold Section */}
|
||||
<div className="flex-1 bg-gradient-to-br from-[#d4af37] via-[#f9d976] to-[#b8860b] relative flex flex-col p-12 overflow-hidden shadow-inner">
|
||||
{/* Stripes Texture Overlay */}
|
||||
<div className="absolute inset-0 opacity-10 pointer-events-none" style={{ backgroundImage: 'repeating-linear-gradient(90deg, transparent, transparent 15px, rgba(0,0,0,0.5) 15px, rgba(0,0,0,0.5) 30px)' }}></div>
|
||||
|
||||
{/* Large Barcode Visual Accents */}
|
||||
<div className="absolute left-8 top-12 bottom-12 w-12 flex flex-col justify-between items-center py-4 border-r border-black/10 opacity-30">
|
||||
<div className="flex flex-col gap-1 w-full px-2">
|
||||
{[...Array(12)].map((_, i) => <div key={i} className={`h-1 w-full bg-black ${i % 3 === 0 ? 'h-2' : 'h-0.5'}`}></div>)}
|
||||
</div>
|
||||
<div className="font-mono text-[8px] font-bold rotate-90 whitespace-nowrap text-black tracking-widest mt-8">#RIT_AUTH_0928374</div>
|
||||
</div>
|
||||
|
||||
<div className="relative pl-16">
|
||||
<h1 className="text-[8rem] md:text-[10rem] font-black text-black leading-none mb-10 tracking-tighter mix-blend-overlay opacity-80 uppercase select-none">TICKET</h1>
|
||||
|
||||
<div className="space-y-4 mb-16">
|
||||
<div className="flex items-baseline gap-6">
|
||||
<h2 className="text-4xl md:text-5xl font-black text-black tracking-tight uppercase">{event.date.split(',')[0]}</h2>
|
||||
<span className="w-12 h-1 bg-black/40"></span>
|
||||
<h2 className="text-4xl md:text-5xl font-black text-black/60 tracking-tight uppercase">{event.schedule?.[0]?.start_time || ''}</h2>
|
||||
</div>
|
||||
<p className="text-xl font-bold text-black/40 uppercase tracking-[0.4em]">{event.location}</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-12 border-t-2 border-black/10 pt-8 mt-12 max-w-xl">
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="text-[10px] font-black text-black/30 uppercase tracking-widest">GATE</span>
|
||||
<span className="text-2xl font-black text-black">A-02</span>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="text-[10px] font-black text-black/30 uppercase tracking-widest">ROW</span>
|
||||
<span className="text-2xl font-black text-black">VIP-0</span>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="text-[10px] font-black text-black/30 uppercase tracking-widest">SEAT</span>
|
||||
<span className="text-2xl font-black text-black">A-42</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* USER DATA DISPLAY AREA */}
|
||||
<div className="mt-16 bg-white/10 backdrop-blur-sm rounded-[2rem] p-8 border border-white/20 shadow-xl max-w-2xl">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
<div>
|
||||
<span className="text-[9px] font-black text-black/40 uppercase tracking-widest block mb-1">Pass Holder</span>
|
||||
<p className="text-xl font-black text-black truncate">{ticket.userName}</p>
|
||||
<p className="text-[11px] font-bold text-black/60">{ticket.userEmail}</p>
|
||||
</div>
|
||||
<div className="text-right md:text-left">
|
||||
<span className="text-[9px] font-black text-black/40 uppercase tracking-widest block mb-1">Academic Dossier</span>
|
||||
<p className="text-lg font-black text-black">{ticket.regNo}</p>
|
||||
<p className="text-[11px] font-bold text-black/60 uppercase">{ticket.dept} • {ticket.section}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6 pt-6 border-t border-black/5 flex items-center justify-between">
|
||||
<div>
|
||||
<span className="text-[9px] font-black text-black/40 uppercase tracking-widest block mb-1">Access Protocol</span>
|
||||
<span className="text-[10px] font-black bg-emerald-500 text-white px-3 py-1 rounded-full">VERIFIED AUTHENTIC</span>
|
||||
</div>
|
||||
<i className="fas fa-shield-check text-2xl text-emerald-600"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Black Sidebar Section */}
|
||||
<div className="w-full md:w-80 bg-black flex flex-col p-10 justify-between relative">
|
||||
<div className="absolute top-0 bottom-0 left-0 w-[1px] bg-white/10 hidden md:block"></div>
|
||||
|
||||
<div className="flex flex-col items-center text-center">
|
||||
<span className="text-[10px] font-black text-white/30 uppercase tracking-[0.5em] vertical-text transform rotate-180 mb-10 hidden md:block" style={{ writingMode: 'vertical-rl' }}>ADMIT ONE TICKET</span>
|
||||
<h3 className="text-4xl font-black text-white leading-none mb-6 vertical-text transform rotate-180 hidden md:block" style={{ writingMode: 'vertical-rl' }}>TICKET</h3>
|
||||
|
||||
<div className="flex flex-col items-center gap-4 mt-8">
|
||||
<div className="w-24 h-24 bg-[#f9d976] p-2 rounded-2xl shadow-[0_10px_30px_rgba(249,217,118,0.2)]">
|
||||
<img src={`https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=VERIFIED_${ticket.ticketId}`} alt="Verified" className="w-full h-full mix-blend-multiply" />
|
||||
</div>
|
||||
<p className="text-amber-400 font-black text-[9px] uppercase tracking-[0.3em]">VIP ENTRY PASS</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center gap-6 mt-12 md:mt-0">
|
||||
<div className="w-full h-24 relative opacity-40">
|
||||
<div className="absolute inset-0 flex gap-1 items-end">
|
||||
{[...Array(20)].map((_, i) => (
|
||||
<div key={i} className="flex-1 bg-white" style={{ height: `${Math.random() * 100}%` }}></div>
|
||||
))}
|
||||
</div>
|
||||
<span className="absolute bottom-[-1.5rem] left-1/2 -translate-x-1/2 font-mono text-[8px] text-white/40 tracking-widest">8 0 1 2 4 9 5 3 0 0 1</span>
|
||||
</div>
|
||||
|
||||
<div className="bg-white/5 border border-white/10 rounded-2xl p-6 w-full group-hover:border-amber-500/30 transition-colors">
|
||||
<span className="text-[8px] font-black text-gray-500 uppercase tracking-widest block mb-2">Event Authority</span>
|
||||
<p className="text-sm font-black text-white uppercase tracking-tight truncate">{event.title}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-12 text-center text-white/30 text-[10px] font-bold uppercase tracking-[0.4em] animate-pulse">
|
||||
Digital Authentication Synchronized with Registrar Office
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TicketVerificationView;
|
||||
162
RIT-EVENT-MANAGEMENT--main/components/UpcomingEventsSlider.tsx
Normal file
162
RIT-EVENT-MANAGEMENT--main/components/UpcomingEventsSlider.tsx
Normal file
@@ -0,0 +1,162 @@
|
||||
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { Event } from '../types';
|
||||
import { CLUBS } from '../constants';
|
||||
|
||||
const Portal: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
return createPortal(children, document.body);
|
||||
};
|
||||
|
||||
interface UpcomingEventsSliderProps {
|
||||
events: Event[];
|
||||
}
|
||||
|
||||
const UpcomingEventsSlider: React.FC<UpcomingEventsSliderProps> = ({ events }) => {
|
||||
const [selectedEvent, setSelectedEvent] = useState<Event | null>(null);
|
||||
const clubInfo = useMemo(() => selectedEvent ? CLUBS.find(c => c.name === selectedEvent.club) : null, [selectedEvent]);
|
||||
|
||||
const sortedEvents = useMemo(() => {
|
||||
return [...events]
|
||||
.filter(e => e.status !== 'Completed')
|
||||
.sort((a, b) => {
|
||||
return new Date(a.date).getTime() - new Date(b.date).getTime();
|
||||
});
|
||||
}, [events]);
|
||||
|
||||
// Duplicate events for seamless loop only if we have enough to fill the track
|
||||
const loopEvents = sortedEvents.length > 2 ? [...sortedEvents, ...sortedEvents] : sortedEvents;
|
||||
|
||||
if (sortedEvents.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%); }
|
||||
}
|
||||
.animate-merry-go-round {
|
||||
animation: merryGoRound 30s linear infinite;
|
||||
display: flex;
|
||||
width: max-content;
|
||||
}
|
||||
.animate-merry-go-round:hover {
|
||||
animation-play-state: paused;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
<div className="flex flex-col lg:flex-row items-start lg:items-end justify-between mb-16 gap-8">
|
||||
<div>
|
||||
<h3 className="text-4xl md:text-5xl font-serif text-[#1A202C] tracking-tight">Upcoming Experiences</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative overflow-hidden w-full">
|
||||
{/* Gradient masks removed to eliminate fogginess */}
|
||||
|
||||
<div className="animate-merry-go-round 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"
|
||||
>
|
||||
<div className="h-48 md:h-56 w-full rounded-[2rem] overflow-hidden mb-6 md:mb-8 relative">
|
||||
<img
|
||||
src={event.image}
|
||||
alt={event.title}
|
||||
className="w-full h-full object-cover transition-transform duration-1000 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>
|
||||
</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">
|
||||
{event.title}
|
||||
</h4>
|
||||
<div className="flex flex-col gap-3 text-gray-500 text-[9px] md:text-[10px] font-bold uppercase tracking-[0.2em]">
|
||||
<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>
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<i className="fas fa-location-dot text-[#f97316]"></i>
|
||||
<span className="truncate">{event.location}</span>
|
||||
</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>
|
||||
<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"
|
||||
>
|
||||
Details <i className="fas fa-arrow-right text-[8px]"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{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">
|
||||
<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"
|
||||
>
|
||||
<i className="fas fa-times text-sm"></i>
|
||||
</button>
|
||||
|
||||
<div className="p-8 overflow-y-auto flex-1 custom-scrollbar">
|
||||
<div className="flex items-center gap-4 mb-8 pb-6 border-b border-gray-100 pr-8">
|
||||
{clubInfo && (
|
||||
<img src={clubInfo.image} className="w-12 h-12 rounded-full object-cover border border-slate-200 shadow-sm" alt="" />
|
||||
)}
|
||||
<div>
|
||||
<span className="block text-[10px] font-black text-[#f97316] uppercase tracking-[0.2em] mb-1">{selectedEvent.club || 'Organized by'}</span>
|
||||
<span className="text-[10px] font-bold text-gray-500 uppercase tracking-widest flex items-center gap-2">
|
||||
<i className="fas fa-user text-[9px]"></i> {selectedEvent.coordinator}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6 pb-6 mt-6 border-t border-gray-100 pt-6">
|
||||
<div className="flex items-center gap-2 mb-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>
|
||||
</div>
|
||||
<div className="relative pt-2">
|
||||
<i className="fas fa-quote-right absolute top-0 right-0 text-5xl text-slate-50 pointer-events-none -z-10"></i>
|
||||
<p className="text-[14px] text-slate-600 leading-relaxed whitespace-pre-wrap font-medium">
|
||||
{selectedEvent.event_summary || "Details for this session will be provided soon."}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 shrink-0 border-t border-gray-100 pt-6">
|
||||
<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"
|
||||
>
|
||||
Close Summary
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Portal>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UpcomingEventsSlider;
|
||||
381
RIT-EVENT-MANAGEMENT--main/components/UserManagementView.tsx
Normal file
381
RIT-EVENT-MANAGEMENT--main/components/UserManagementView.tsx
Normal file
@@ -0,0 +1,381 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { supabase, createAdminClient } from '../supabase';
|
||||
|
||||
const DEPARTMENTS = ['AIDS', 'CSBS', 'CSE', 'CCE', 'MECH', 'VLSI', 'BIO-TECH', 'AIML', 'ECE', 'H&S'];
|
||||
|
||||
const UserManagementView: React.FC = () => {
|
||||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
email: '',
|
||||
department: '',
|
||||
phone: '',
|
||||
role: 'Faculty',
|
||||
password: '',
|
||||
});
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [success, setSuccess] = useState<string | null>(null);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [coordinators, setCoordinators] = useState<any[]>([]);
|
||||
const [isLoadingList, setIsLoadingList] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
fetchCoordinators();
|
||||
}, []);
|
||||
|
||||
const fetchCoordinators = async () => {
|
||||
try {
|
||||
const { data: adminData, error: adminError } = await supabase
|
||||
.from('Facultyusers')
|
||||
.select('*')
|
||||
.order('name', { ascending: true });
|
||||
|
||||
if (adminError && !adminError.message.includes('not found')) throw adminError;
|
||||
|
||||
// Sort by role: HOD first, then Faculty. Alphabetical within roles.
|
||||
const sorted = (adminData || []).sort((a, b) => {
|
||||
if (a.role === 'HOD' && b.role !== 'HOD') return -1;
|
||||
if (a.role !== 'HOD' && b.role === 'HOD') return 1;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
|
||||
setCoordinators(sorted);
|
||||
} catch (err) {
|
||||
console.error("Error fetching coordinators:", err);
|
||||
} finally {
|
||||
setIsLoadingList(false);
|
||||
}
|
||||
};
|
||||
|
||||
const generatePassword = () => {
|
||||
const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*";
|
||||
let pass = "";
|
||||
for (let i = 0; i < 12; i++) {
|
||||
pass += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
setFormData(prev => ({ ...prev, password: pass }));
|
||||
setShowPassword(true);
|
||||
};
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData(prev => ({ ...prev, [name]: value }));
|
||||
};
|
||||
|
||||
const handleDeleteUser = async (id: string, name: string, email: string) => {
|
||||
if (!window.confirm(`Are you sure you want to remove ${name}? This will delete their profile but they may still exist in Auth.`)) return;
|
||||
|
||||
try {
|
||||
const { error } = await supabase.from('Facultyusers').delete().eq('id', id);
|
||||
if (error) throw error;
|
||||
|
||||
setCoordinators(prev => prev.filter(c => c.id !== id));
|
||||
} catch (err: any) {
|
||||
alert("Error deleting user: " + err.message);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setIsSubmitting(true);
|
||||
setError(null);
|
||||
setSuccess(null);
|
||||
|
||||
try {
|
||||
// Use a separate client that doesn't persist sessions to avoid logging out the admin
|
||||
const adminClient = createAdminClient();
|
||||
|
||||
const { data, error: signUpError } = await adminClient.auth.signUp({
|
||||
email: formData.email,
|
||||
password: formData.password,
|
||||
options: {
|
||||
data: {
|
||||
name: formData.name,
|
||||
role: formData.role === 'HOD' ? 'HOD' : 'COORDINATOR',
|
||||
department: formData.department,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (signUpError) throw signUpError;
|
||||
|
||||
if (data.user) {
|
||||
const { error: adminDbError } = await supabase.from('Facultyusers').insert({
|
||||
id: data.user.id,
|
||||
name: formData.name,
|
||||
email: formData.email,
|
||||
phone: formData.phone,
|
||||
department: formData.department,
|
||||
role: formData.role,
|
||||
updated_at: new Date().toISOString()
|
||||
});
|
||||
|
||||
if (adminDbError) throw adminDbError;
|
||||
|
||||
setSuccess(`User ${formData.name} created successfully! Password: ${formData.password}`);
|
||||
setFormData({ name: '', email: '', department: '', phone: '', role: 'Faculty', password: '' });
|
||||
fetchCoordinators();
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error("User creation error:", err);
|
||||
setError(err.message || "Failed to create user.");
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="animate-in fade-in slide-in-from-bottom-10 duration-700">
|
||||
<div className="mb-10">
|
||||
<h2 className="text-4xl font-black text-slate-900 uppercase tracking-tighter mb-2">
|
||||
USER <span className="text-sky-500">MANAGEMENT</span>
|
||||
</h2>
|
||||
<p className="text-slate-400 text-xs font-bold uppercase tracking-widest">
|
||||
Provision access for Event Coordinators
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid lg:grid-cols-2 gap-10">
|
||||
{/* Creation Card */}
|
||||
<div className="bg-white rounded-[2.5rem] border border-slate-100 p-10 shadow-xl shadow-slate-200/50 relative overflow-hidden group">
|
||||
<div className="absolute top-0 right-0 w-32 h-32 bg-sky-500/5 rounded-full -translate-y-1/2 translate-x-1/2 group-hover:scale-150 transition-transform duration-700"></div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6 relative z-10">
|
||||
<div className="grid gap-6">
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-2">Full Name</label>
|
||||
<input
|
||||
required
|
||||
name="name"
|
||||
value={formData.name}
|
||||
onChange={handleInputChange}
|
||||
placeholder="e.g. Dr. John Doe"
|
||||
className="w-full bg-slate-50 border-none rounded-2xl px-6 py-4 text-xs font-bold outline-none focus:ring-2 focus:ring-sky-500 transition-all"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-2">Assign Role</label>
|
||||
<div className="flex bg-slate-50 p-1.5 rounded-2xl gap-2">
|
||||
{['Faculty', 'HOD'].map(r => (
|
||||
<button
|
||||
key={r}
|
||||
type="button"
|
||||
onClick={() => setFormData(p => ({ ...p, role: r }))}
|
||||
className={`flex-1 py-3 rounded-xl text-[9px] font-black uppercase tracking-widest transition-all ${
|
||||
formData.role === r
|
||||
? 'bg-white text-sky-600 shadow-sm'
|
||||
: 'text-slate-400 hover:text-slate-600'
|
||||
}`}
|
||||
>
|
||||
{r}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-2">Gmail Address</label>
|
||||
<input
|
||||
required
|
||||
type="email"
|
||||
name="email"
|
||||
value={formData.email}
|
||||
onChange={handleInputChange}
|
||||
placeholder="coordinator@gmail.com"
|
||||
className="w-full bg-slate-50 border-none rounded-2xl px-6 py-4 text-xs font-bold outline-none focus:ring-2 focus:ring-sky-500 transition-all"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-2">Phone Number</label>
|
||||
<input
|
||||
required
|
||||
name="phone"
|
||||
value={formData.phone}
|
||||
onChange={handleInputChange}
|
||||
placeholder="e.g. 9876543210"
|
||||
className="w-full bg-slate-50 border-none rounded-2xl px-6 py-4 text-xs font-bold outline-none focus:ring-2 focus:ring-sky-500 transition-all"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-2">Department</label>
|
||||
<select
|
||||
required
|
||||
name="department"
|
||||
value={formData.department}
|
||||
onChange={handleInputChange}
|
||||
className="w-full bg-slate-50 border-none rounded-2xl px-6 py-4 text-xs font-bold outline-none focus:ring-2 focus:ring-sky-500 transition-all appearance-none"
|
||||
>
|
||||
<option value="">Select Dept</option>
|
||||
{DEPARTMENTS.map(dept => <option key={dept} value={dept}>{dept}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between items-center px-2">
|
||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Password</label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={generatePassword}
|
||||
className="text-[8px] font-black text-sky-500 uppercase hover:underline"
|
||||
>
|
||||
Generate
|
||||
</button>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<input
|
||||
required
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
name="password"
|
||||
value={formData.password}
|
||||
onChange={handleInputChange}
|
||||
placeholder="••••••••"
|
||||
className="w-full bg-slate-50 border-none rounded-2xl px-6 py-4 text-xs font-bold outline-none focus:ring-2 focus:ring-sky-500 transition-all"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute right-4 top-1/2 -translate-y-1/2 text-slate-300 hover:text-sky-500 transition-colors"
|
||||
>
|
||||
<i className={`fas ${showPassword ? 'fa-eye-slash' : 'fa-eye'}`}></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="p-4 bg-rose-50 border border-rose-100 rounded-2xl flex items-center gap-3 text-rose-600 text-[10px] font-bold uppercase">
|
||||
<i className="fas fa-exclamation-circle"></i>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{success && (
|
||||
<div className="p-4 bg-emerald-50 border border-emerald-100 rounded-2xl flex items-center gap-3 text-emerald-600 text-[10px] font-bold uppercase">
|
||||
<i className="fas fa-check-circle"></i>
|
||||
{success}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="w-full py-5 bg-sky-500 text-white rounded-2xl font-black uppercase text-xs tracking-[0.2em] shadow-xl shadow-sky-500/20 hover:bg-sky-600 transition-all active:scale-95 disabled:opacity-50"
|
||||
>
|
||||
{isSubmitting ? 'Creating User...' : 'Create Event Coordinator'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* Info Card */}
|
||||
<div className="space-y-6">
|
||||
<div className="bg-slate-900 rounded-[2.5rem] p-10 text-white relative overflow-hidden">
|
||||
<div className="absolute top-0 right-0 p-10 opacity-10">
|
||||
<i className="fas fa-user-shield text-9xl"></i>
|
||||
</div>
|
||||
<h3 className="text-2xl font-black uppercase tracking-tight mb-4 relative z-10">Security Protocol</h3>
|
||||
<p className="text-slate-400 text-sm font-medium leading-relaxed mb-8 relative z-10">
|
||||
Coordinators created here will have immediate access to the Event Coordinator Portal.
|
||||
They will be stored in the <code className="text-sky-400 bg-sky-400/10 px-2 py-1 rounded">Facultyusers</code> table.
|
||||
</p>
|
||||
<ul className="space-y-4 relative z-10">
|
||||
{[
|
||||
'Automatic role assignment (ADMIN)',
|
||||
'Departmental scoping enabled',
|
||||
'Profile synchronization active',
|
||||
'Sign-up restricted for public'
|
||||
].map((text, i) => (
|
||||
<li key={i} className="flex items-center gap-3 text-[10px] font-bold uppercase tracking-widest text-slate-300">
|
||||
<i className="fas fa-check text-sky-500"></i>
|
||||
{text}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="bg-sky-50 rounded-[2.5rem] p-10 border border-sky-100">
|
||||
<h4 className="text-sky-900 font-black uppercase text-xs tracking-widest mb-4">Important Note</h4>
|
||||
<p className="text-sky-700/70 text-xs font-medium leading-relaxed">
|
||||
Creating a user via this interface will register them in Supabase Auth.
|
||||
Ensure the Gmail address is valid as it will be used for portal access.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-20">
|
||||
<div className="mb-8">
|
||||
<h3 className="text-2xl font-black text-slate-900 uppercase tracking-tighter">
|
||||
EXISTING <span className="text-sky-500">COORDINATORS</span>
|
||||
</h3>
|
||||
<p className="text-slate-400 text-[10px] font-bold uppercase tracking-widest">
|
||||
Manage active event coordinators
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{isLoadingList ? (
|
||||
<div className="flex justify-center py-20">
|
||||
<div className="w-8 h-8 border-4 border-sky-500 border-t-transparent rounded-full animate-spin"></div>
|
||||
</div>
|
||||
) : coordinators.length === 0 ? (
|
||||
<div className="bg-slate-50 rounded-[2rem] p-12 text-center border-2 border-dashed border-slate-200">
|
||||
<p className="text-slate-400 font-bold uppercase text-xs tracking-widest">No coordinators found</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{coordinators.map((coord) => (
|
||||
<div key={coord.id} className="bg-white rounded-3xl border border-slate-100 p-6 shadow-sm hover:shadow-md transition-all group relative overflow-hidden">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 rounded-2xl bg-sky-500/10 flex items-center justify-center text-sky-500">
|
||||
<i className="fas fa-user-tie text-xl"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-black text-slate-900 uppercase text-xs tracking-tight">{coord.name}</h4>
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="text-sky-500 text-[9px] font-black uppercase tracking-widest">
|
||||
{coord.department || coord.dept}
|
||||
</p>
|
||||
<span className={`text-[7px] font-black uppercase px-2 py-0.5 rounded-full ${
|
||||
coord.role === 'HOD' ? 'bg-orange-500 text-white' : 'bg-slate-100 text-slate-400'
|
||||
}`}>
|
||||
{coord.role || 'Faculty'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleDeleteUser(coord.id, coord.name, coord.email)}
|
||||
className="w-8 h-8 rounded-xl bg-rose-50 text-rose-500 flex items-center justify-center hover:bg-rose-500 hover:text-white transition-all"
|
||||
>
|
||||
<i className="fas fa-trash-alt text-xs"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 text-slate-400">
|
||||
<i className="fas fa-envelope text-[10px]"></i>
|
||||
<span className="text-[10px] font-medium">{coord.email}</span>
|
||||
</div>
|
||||
{coord.phone && (
|
||||
<div className="flex items-center gap-2 text-slate-400">
|
||||
<i className="fas fa-phone text-[10px]"></i>
|
||||
<span className="text-[10px] font-medium">{coord.phone}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="absolute bottom-0 right-0 p-4 opacity-5 group-hover:opacity-10 transition-opacity">
|
||||
<i className="fas fa-id-badge text-6xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserManagementView;
|
||||
101
RIT-EVENT-MANAGEMENT--main/components/WelcomeScreen.tsx
Normal file
101
RIT-EVENT-MANAGEMENT--main/components/WelcomeScreen.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
|
||||
import React from 'react';
|
||||
import { UserRole, Event } from '../types';
|
||||
|
||||
interface WelcomeScreenProps {
|
||||
onSelectRole: (role: UserRole) => void;
|
||||
events: Event[];
|
||||
}
|
||||
|
||||
const WelcomeScreen: React.FC<WelcomeScreenProps> = ({ onSelectRole, events }) => {
|
||||
const portals = [
|
||||
{
|
||||
id: 'STUDENT' as UserRole,
|
||||
title: 'Student Portal',
|
||||
description: 'Access events, track registrations, and manage your academic profile.',
|
||||
icon: 'fa-user-graduate',
|
||||
color: 'bg-[#004a99]',
|
||||
hoverColor: 'hover:border-[#004a99]',
|
||||
textColor: 'text-[#004a99]'
|
||||
},
|
||||
{
|
||||
id: 'COORDINATOR' as UserRole,
|
||||
title: 'Event Coordinator Portal',
|
||||
description: 'Create, manage, and coordinate campus events and announcements.',
|
||||
icon: 'fa-calendar-check',
|
||||
color: 'bg-orange-500',
|
||||
hoverColor: 'hover:border-orange-500',
|
||||
textColor: 'text-orange-500'
|
||||
},
|
||||
{
|
||||
id: 'ADMIN' as UserRole,
|
||||
title: 'Admin Portal',
|
||||
description: 'System-wide oversight, user management, and high-level analytics.',
|
||||
icon: 'fa-user-shield',
|
||||
color: 'bg-[#004a99]',
|
||||
hoverColor: 'hover:border-[#004a99]',
|
||||
textColor: 'text-[#004a99]'
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen w-full bg-white flex flex-col items-center justify-center p-6 relative overflow-hidden">
|
||||
{/* Background Decorative Elements */}
|
||||
<div className="absolute top-[-10%] left-[-10%] w-96 h-96 bg-[#004a99]/5 rounded-full blur-[100px] opacity-60"></div>
|
||||
<div className="absolute bottom-[-10%] right-[-10%] w-96 h-96 bg-orange-50 rounded-full blur-[100px] opacity-60"></div>
|
||||
|
||||
<div className="relative z-10 w-full max-w-6xl">
|
||||
<div className="text-center mb-16 animate-in fade-in slide-in-from-top-10 duration-700">
|
||||
<img
|
||||
src="https://www.facultyplus.com/wp-content/uploads/2020/11/Rit-logo.png"
|
||||
alt="RIT Logo"
|
||||
className="h-16 md:h-20 w-auto mx-auto mb-8"
|
||||
/>
|
||||
<p className="text-slate-500 text-lg font-medium max-w-2xl mx-auto uppercase tracking-widest text-xs">
|
||||
Select your gateway to excellence. Connect, manage, and celebrate.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-3 gap-8">
|
||||
{portals.map((portal, index) => (
|
||||
<div
|
||||
key={portal.id}
|
||||
onClick={() => onSelectRole(portal.id)}
|
||||
className={`group relative bg-white border-2 border-slate-100 rounded-[3rem] p-10 cursor-pointer transition-all duration-500 hover:-translate-y-4 hover:shadow-[0_40px_80px_-20px_rgba(0,0,0,0.1)] ${portal.hoverColor} animate-in fade-in slide-in-from-bottom-10`}
|
||||
style={{ animationDelay: `${index * 150}ms` }}
|
||||
>
|
||||
<div className={`w-20 h-20 ${portal.color} rounded-3xl flex items-center justify-center mb-8 shadow-lg group-hover:scale-110 transition-transform duration-500`}>
|
||||
<i className={`fas ${portal.icon} text-3xl text-white`}></i>
|
||||
</div>
|
||||
|
||||
<h3 className="text-2xl font-black text-slate-900 uppercase tracking-tight mb-4 group-hover:text-orange-500 transition-colors">
|
||||
{portal.title}
|
||||
</h3>
|
||||
|
||||
<p className="text-slate-500 text-sm font-medium leading-relaxed mb-8">
|
||||
{portal.description}
|
||||
</p>
|
||||
|
||||
<div className={`flex items-center gap-3 text-xs font-black uppercase tracking-widest ${portal.textColor} group-hover:gap-5 transition-all`}>
|
||||
Enter Portal <i className="fas fa-arrow-right"></i>
|
||||
</div>
|
||||
|
||||
{/* Decorative Corner */}
|
||||
<div className="absolute top-0 right-0 p-8 opacity-0 group-hover:opacity-10 transition-opacity">
|
||||
<i className={`fas ${portal.icon} text-8xl`}></i>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-20 text-center animate-in fade-in duration-1000 delay-500">
|
||||
<p className="text-[10px] font-black text-slate-400 uppercase tracking-[0.5em]">
|
||||
© 2024 Rajalakshmi Institute of Technology • Academic Excellence
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WelcomeScreen;
|
||||
198
RIT-EVENT-MANAGEMENT--main/constants.tsx
Normal file
198
RIT-EVENT-MANAGEMENT--main/constants.tsx
Normal file
@@ -0,0 +1,198 @@
|
||||
|
||||
import { Event } from './types';
|
||||
|
||||
const getFutureDate = (days: number, hours: number = 0) => {
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() + days);
|
||||
date.setHours(date.getHours() + hours);
|
||||
const options: Intl.DateTimeFormatOptions = { month: 'short', day: '2-digit', year: 'numeric' };
|
||||
return date.toLocaleDateString('en-US', options);
|
||||
};
|
||||
|
||||
export const ALL_EVENTS: Event[] = [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Next-Gen AI Forum',
|
||||
date: getFutureDate(3, 4),
|
||||
|
||||
location: 'Innovation Lab',
|
||||
category: 'TECHNICAL',
|
||||
domain: 'Computer Science and Engineering',
|
||||
club: 'Techspark',
|
||||
pricingType: 'PAID',
|
||||
coordinator: 'Dr. Ramesh Kumar',
|
||||
image: 'https://images.unsplash.com/photo-1620712943543-bcc4628c9759?auto=format&fit=crop&q=80&w=800'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'Web Architecture 2025',
|
||||
date: getFutureDate(1, 2),
|
||||
location: 'Seminar Hall A',
|
||||
category: 'TECHNICAL',
|
||||
domain: 'Information Technology',
|
||||
club: 'Infintus club',
|
||||
pricingType: 'FREE',
|
||||
coordinator: 'Prof. Anitha S.',
|
||||
image: 'https://images.unsplash.com/photo-1517694712202-14dd9538aa97?auto=format&fit=crop&q=80&w=800'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
title: 'Cultural Night Jam',
|
||||
date: getFutureDate(15),
|
||||
location: 'Main Auditorium',
|
||||
category: 'NON-TECHNICAL',
|
||||
domain: 'Arts & Culture',
|
||||
club: 'Euphoria club',
|
||||
pricingType: 'PAID',
|
||||
coordinator: 'Dr. Sivakumar P.',
|
||||
image: 'https://images.unsplash.com/photo-1492684223066-81342ee5ff30?auto=format&fit=crop&q=80&w=800'
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
title: 'FullStack React Workshop',
|
||||
date: getFutureDate(5, 1),
|
||||
location: 'Computer Lab 3',
|
||||
category: 'WORKSHOP',
|
||||
domain: 'Software Engineering',
|
||||
club: 'Fusion club',
|
||||
pricingType: 'FREE',
|
||||
coordinator: 'Prof. Rajesh B.',
|
||||
image: 'https://images.unsplash.com/photo-1633356122544-f134324a6cee?auto=format&fit=crop&q=80&w=800'
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
title: 'Entrepreneurship 101',
|
||||
date: getFutureDate(12, 8),
|
||||
location: 'Management Block',
|
||||
category: 'NON-TECHNICAL',
|
||||
domain: 'Management',
|
||||
club: 'EDC club',
|
||||
pricingType: 'FREE',
|
||||
coordinator: 'Dr. Preethi M.',
|
||||
image: 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?auto=format&fit=crop&q=80&w=800'
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
title: 'Robo-Wars 2025',
|
||||
date: getFutureDate(20),
|
||||
location: 'Mechanical Workshop',
|
||||
category: 'TECHNICAL',
|
||||
domain: 'Mechanical',
|
||||
club: 'Steam',
|
||||
pricingType: 'PAID',
|
||||
coordinator: 'Prof. Karthikeyan J.',
|
||||
image: 'https://images.unsplash.com/photo-1561144443-0559f2e4bd3c?auto=format&fit=crop&q=80&w=800'
|
||||
}
|
||||
];
|
||||
|
||||
export const CATEGORIES = [
|
||||
{ name: 'Technical', id: 'TECHNICAL', image: 'https://images.unsplash.com/photo-1518770660439-4636190af475?auto=format&fit=crop&q=80&w=800' },
|
||||
{ name: 'Non-Technical', id: 'NON-TECHNICAL', image: 'https://images.unsplash.com/photo-1523580494863-6f3031224c94?auto=format&fit=crop&q=80&w=800' },
|
||||
{ name: 'Workshops', id: 'WORKSHOP', image: 'https://images.unsplash.com/photo-1552664730-d307ca884978?auto=format&fit=crop&q=80&w=800' },
|
||||
{ name: 'Centre Activity', id: 'CENTRE-ACTIVITY', image: 'https://images.unsplash.com/photo-1540575467063-178a50c2df87?auto=format&fit=crop&q=80&w=1200' }
|
||||
];
|
||||
|
||||
export const CLUBS = [
|
||||
{ name: 'Fusion club', image: 'https://github.com/Sachin-627/club/raw/main/Fusion%20club.jpeg' },
|
||||
{ name: 'Helios club', image: 'https://github.com/Sachin-627/club/raw/main/Helios%20club.jpeg' },
|
||||
{ name: 'Infintus club', image: 'https://github.com/Sachin-627/club/raw/main/Infintus%20club.jpeg' },
|
||||
{ name: 'Mediastic', image: 'https://github.com/Sachin-627/club/raw/main/Mediastic.jpeg' },
|
||||
{ name: 'Nippon Club', image: 'https://github.com/Sachin-627/club/raw/main/Nippon%20Club.jpeg' },
|
||||
{ name: 'Pod x club', image: 'https://github.com/Sachin-627/club/raw/main/Pod%20x%20club.jpeg' },
|
||||
{ name: 'Rotract', image: 'https://github.com/Sachin-627/club/raw/main/Rotract.jpeg' },
|
||||
{ name: 'Unnat Bharath Abhiyan club', image: 'https://github.com/Sachin-627/club/raw/main/UBS.jpeg' },
|
||||
{ name: 'Variti club', image: 'https://github.com/Sachin-627/club/raw/main/Variti%20club.jpeg' },
|
||||
{ name: 'Wec club', image: 'https://github.com/Sachin-627/club/raw/main/Wec%20club.jpeg' },
|
||||
{ name: 'Wistom club', image: 'https://github.com/Sachin-627/club/raw/main/Wistom%20club.jpeg' },
|
||||
{ name: 'Yatra club', image: 'https://github.com/Sachin-627/club/raw/main/Yatra%20club.jpeg' },
|
||||
{ name: 'Yuva club', image: 'https://github.com/Sachin-627/club/raw/main/Yuva%20club.jpeg' },
|
||||
{ name: 'Artist League', image: 'https://github.com/Sachin-627/club/raw/main/artist%20league.jpeg' },
|
||||
{ name: 'Classical club', image: 'https://github.com/Sachin-627/club/raw/main/classical%20club.jpeg' },
|
||||
{ name: 'EDC club', image: 'https://github.com/Sachin-627/club/raw/main/edc%20club.jpeg' },
|
||||
{ name: 'Euphoria club', image: 'https://github.com/Sachin-627/club/raw/main/euphoria%20club.jpeg' },
|
||||
{ name: 'R Square', image: 'https://github.com/Sachin-627/club/raw/main/r%20square.jpeg' },
|
||||
{ name: 'Raptology', image: 'https://github.com/Sachin-627/club/raw/main/raptology.jpeg' },
|
||||
{ name: 'Steam', image: 'https://github.com/Sachin-627/club/raw/main/steam.jpeg' },
|
||||
{ name: 'Techspark', image: 'https://github.com/Sachin-627/club/raw/main/techspark.jpeg' },
|
||||
{ name: 'YRC club', image: 'https://github.com/Sachin-627/club/raw/main/yrc%20club.jpeg' },
|
||||
{ name: 'NCC club', image: 'https://github.com/Sachin-627/club/raw/main/ncc%20club.jpeg' }
|
||||
];
|
||||
|
||||
export const DOMAIN_MAP: Record<string, { id: string, name: string, image: string }[]> = {
|
||||
'TECHNICAL': [
|
||||
{
|
||||
id: 'CSE',
|
||||
name: 'CSE',
|
||||
image: 'https://images.unsplash.com/photo-1517694712202-14dd9538aa97?auto=format&fit=crop&q=80&w=600'
|
||||
},
|
||||
{
|
||||
id: 'CSBS',
|
||||
name: 'CSBS',
|
||||
image: 'https://images.unsplash.com/photo-1460925895917-afdab827c52f?auto=format&fit=crop&q=80&w=600'
|
||||
},
|
||||
{
|
||||
id: 'AIML',
|
||||
name: 'AIML',
|
||||
image: 'https://images.unsplash.com/photo-1677442136019-21780ecad995?auto=format&fit=crop&q=80&w=600'
|
||||
},
|
||||
{
|
||||
id: 'AIDS',
|
||||
name: 'AIDS',
|
||||
image: 'https://images.unsplash.com/photo-1509228627152-72ae9ae6848d?auto=format&fit=crop&q=80&w=600'
|
||||
},
|
||||
{
|
||||
id: 'IT',
|
||||
name: 'IT',
|
||||
image: 'https://images.unsplash.com/photo-1498050108023-c5249f4df085?auto=format&fit=crop&q=80&w=600'
|
||||
},
|
||||
{
|
||||
id: 'ECE',
|
||||
name: 'ECE',
|
||||
image: 'https://images.unsplash.com/photo-1517077304055-6e89abbf09b0?auto=format&fit=crop&q=80&w=600'
|
||||
},
|
||||
{
|
||||
id: 'Mechanical',
|
||||
name: 'Mechanical',
|
||||
image: 'https://images.unsplash.com/photo-1537462715879-360eeb61a0ad?auto=format&fit=crop&q=80&w=600'
|
||||
},
|
||||
{
|
||||
id: 'Bio-tech',
|
||||
name: 'Bio-tech',
|
||||
image: 'https://images.unsplash.com/photo-1530210124550-912dc1381cb8?auto=format&fit=crop&q=80&w=600'
|
||||
},
|
||||
{
|
||||
id: 'CCE',
|
||||
name: 'CCE',
|
||||
image: 'https://images.unsplash.com/photo-1550751827-4bd374c3f58b?auto=format&fit=crop&q=80&w=600'
|
||||
},
|
||||
{
|
||||
id: 'VLSI',
|
||||
name: 'VLSI',
|
||||
image: 'https://images.unsplash.com/photo-1518770660439-4636190af475?auto=format&fit=crop&q=80&w=600'
|
||||
}
|
||||
],
|
||||
'NON-TECHNICAL': [
|
||||
{ id: 'Management', name: 'Management', image: 'https://images.unsplash.com/photo-1519389950473-47ba0277781c?auto=format&fit=crop&q=80&w=400' },
|
||||
{ id: 'Arts & Culture', name: 'Arts & Culture', image: 'https://images.unsplash.com/photo-1513364776144-60967b0f800f?auto=format&fit=crop&q=80&w=800' },
|
||||
{ id: 'Sports', name: 'Sports', image: 'https://images.unsplash.com/photo-1552674605-db6ffd4facb5?auto=format&fit=crop&q=80&w=400' },
|
||||
{ id: 'Social Welfare', name: 'Social Welfare', image: 'https://images.unsplash.com/photo-1488521787991-ed7bbaae773c?auto=format&fit=crop&q=80&w=400' }
|
||||
],
|
||||
'WORKSHOP': [
|
||||
{ id: 'Software Dev', name: 'Software Dev', image: 'https://images.unsplash.com/photo-1633356122544-f134324a6cee?auto=format&fit=crop&q=80&w=400' },
|
||||
{ id: 'Design', name: 'Design', image: 'https://images.unsplash.com/photo-1561070791-2526d30994b5?auto=format&fit=crop&q=80&w=800' },
|
||||
{ id: 'Cloud/DevOps', name: 'Cloud/DevOps', image: 'https://images.unsplash.com/photo-1451187580459-43490279c0fa?auto=format&fit=crop&q=80&w=400' }
|
||||
],
|
||||
'CENTRE-ACTIVITY': [
|
||||
{ id: 'Research Hub', name: 'Research Hub', image: 'https://images.unsplash.com/photo-1507679799987-c73779587ccf?auto=format&fit=crop&q=80&w=400' },
|
||||
{ id: 'Innovation Cell', name: 'Innovation Cell', image: 'https://images.unsplash.com/photo-1581091226825-a6a2a5aee158?auto=format&fit=crop&q=80&w=400' },
|
||||
{ id: 'Incubation Centre', name: 'Incubation Centre', image: 'https://images.unsplash.com/photo-1522071820081-009f0129c71c?auto=format&fit=crop&q=80&w=400' }
|
||||
],
|
||||
'ALL': [
|
||||
{ id: 'CSE', name: 'CSE', image: 'https://images.unsplash.com/photo-1555255707-c07966488bc0?auto=format&fit=crop&q=80&w=400' },
|
||||
{ id: 'IT', name: 'IT', image: 'https://images.unsplash.com/photo-1498050108023-c5249f4df085?auto=format&fit=crop&q=80&w=400' },
|
||||
{ id: 'AIML', name: 'AIML', image: 'https://images.unsplash.com/photo-1677442136019-21780ecad995?auto=format&fit=crop&q=80&w=400' },
|
||||
{ id: 'AIDS', name: 'AIDS', image: 'https://images.unsplash.com/photo-1551288049-bbbda5366391?auto=format&fit=crop&q=80&w=400' },
|
||||
{ id: 'Management', name: 'Management', image: 'https://images.unsplash.com/photo-1519389950473-47ba0277781c?auto=format&fit=crop&q=80&w=400' },
|
||||
{ id: 'Mechanical', name: 'Mechanical', image: 'https://images.unsplash.com/photo-1537462715879-360eeb61a0ad?auto=format&fit=crop&q=80&w=400' }
|
||||
]
|
||||
};
|
||||
BIN
RIT-EVENT-MANAGEMENT--main/galaxy_bg.jpg
Normal file
BIN
RIT-EVENT-MANAGEMENT--main/galaxy_bg.jpg
Normal file
Binary file not shown.
123
RIT-EVENT-MANAGEMENT--main/index.css
Normal file
123
RIT-EVENT-MANAGEMENT--main/index.css
Normal file
@@ -0,0 +1,123 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
body {
|
||||
@apply bg-white text-black font-sans;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes sway {
|
||||
0% { transform: rotate(-1deg); }
|
||||
50% { transform: rotate(1deg); }
|
||||
100% { transform: rotate(-1deg); }
|
||||
}
|
||||
|
||||
@keyframes mist {
|
||||
0% { transform: translateX(-5%); opacity: 0.3; }
|
||||
50% { transform: translateX(5%); opacity: 0.5; }
|
||||
100% { transform: translateX(-5%); opacity: 0.3; }
|
||||
}
|
||||
|
||||
@keyframes breeze {
|
||||
0% { transform: translateY(0) scale(1); }
|
||||
50% { transform: translateY(-5px) scale(1.02); }
|
||||
100% { transform: translateY(0) scale(1); }
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0% { transform: translateY(0px); }
|
||||
50% { transform: translateY(-20px); }
|
||||
100% { transform: translateY(0px); }
|
||||
}
|
||||
|
||||
.animate-sway {
|
||||
animation: sway 8s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.animate-mist {
|
||||
animation: mist 15s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.animate-breeze {
|
||||
animation: breeze 10s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.animate-float {
|
||||
animation: float 6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.glass-navbar {
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.layered-bg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: -1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.shape {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
filter: blur(80px);
|
||||
}
|
||||
|
||||
.shape-1 {
|
||||
width: 600px;
|
||||
height: 600px;
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
top: -200px;
|
||||
left: -200px;
|
||||
}
|
||||
|
||||
.shape-2 {
|
||||
width: 500px;
|
||||
height: 500px;
|
||||
background: rgba(0, 0, 0, 0.03);
|
||||
bottom: -150px;
|
||||
right: -150px;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-track {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.hero-gradient {
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
||||
}
|
||||
|
||||
.text-stroke {
|
||||
-webkit-text-stroke: 1px black;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.scrollbar-hide::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.scrollbar-hide {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
88
RIT-EVENT-MANAGEMENT--main/index.html
Normal file
88
RIT-EVENT-MANAGEMENT--main/index.html
Normal file
@@ -0,0 +1,88 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>RIT Events Hub | Professional Portal</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" crossorigin="anonymous" />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=Playfair+Display:ital,wght@0,400;0,700;0,900;1,400&family=Pirata+One&family=Almendra:ital,wght@0,400;0,700;1,400;1,700&display=swap" rel="stylesheet" crossorigin="anonymous">
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
.font-serif {
|
||||
font-family: 'Playfair Display', serif;
|
||||
}
|
||||
|
||||
.font-sans {
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
.font-medieval {
|
||||
font-family: 'Pirata One', cursive;
|
||||
}
|
||||
|
||||
.font-parchment {
|
||||
font-family: 'Almendra', serif;
|
||||
}
|
||||
|
||||
/* Global Scrollbar Hide */
|
||||
::-webkit-scrollbar {
|
||||
width: 0px;
|
||||
height: 0px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
* {
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
|
||||
.no-scrollbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.bg-gradient-mesh {
|
||||
background: radial-gradient(at 0% 0%, hsla(253,16%,7%,1) 0, transparent 50%),
|
||||
radial-gradient(at 50% 0%, hsla(225,39%,30%,1) 0, transparent 50%),
|
||||
radial-gradient(at 100% 0%, hsla(339,49%,30%,1) 0, transparent 50%);
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
input::-ms-reveal,
|
||||
input::-ms-clear {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
.page-transition {
|
||||
animation: fadeIn 0.4s ease-out;
|
||||
}
|
||||
</style>
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
"react-dom/": "https://esm.sh/react-dom@^19.2.3/",
|
||||
"react/": "https://esm.sh/react@^19.2.3/",
|
||||
"react": "https://esm.sh/react@^19.2.3",
|
||||
"react-dom": "https://esm.sh/react-dom@^19.2.3",
|
||||
"html-to-image": "https://esm.sh/html-to-image@1.11.11",
|
||||
"@supabase/supabase-js": "https://esm.sh/@supabase/supabase-js@2.39.7"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<link rel="stylesheet" href="/index.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
16
RIT-EVENT-MANAGEMENT--main/index.tsx
Normal file
16
RIT-EVENT-MANAGEMENT--main/index.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
|
||||
const rootElement = document.getElementById('root');
|
||||
if (!rootElement) {
|
||||
throw new Error("Could not find root element to mount to");
|
||||
}
|
||||
|
||||
const root = ReactDOM.createRoot(rootElement);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
5
RIT-EVENT-MANAGEMENT--main/metadata.json
Normal file
5
RIT-EVENT-MANAGEMENT--main/metadata.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "RIT EVENTS HUB",
|
||||
"description": "A professional event management platform for students and faculty featuring event discovery, booking, and profile management.",
|
||||
"requestFramePermissions": []
|
||||
}
|
||||
85
RIT-EVENT-MANAGEMENT--main/migrateDomains.js
Normal file
85
RIT-EVENT-MANAGEMENT--main/migrateDomains.js
Normal file
@@ -0,0 +1,85 @@
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
|
||||
const SUPABASE_URL = 'https://mhvdpopbbtllhvzcpqkf.supabase.co';
|
||||
const SUPABASE_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im1odmRwb3BiYnRsbGh2emNwcWtmIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzE0MjUwMTUsImV4cCI6MjA4NzAwMTAxNX0.59OvUt4dj8-OguyD6dItdhC2jvUPSJVXNbsUxdk9w_s';
|
||||
|
||||
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);
|
||||
|
||||
const DOMAIN_MAP = {
|
||||
'TECHNICAL': [
|
||||
{ id: 'Computer Science and Engineering', name: 'CSE', image: 'https://images.unsplash.com/photo-1517694712202-14dd9538aa97?auto=format&fit=crop&q=80&w=600' },
|
||||
{ id: 'Computer Science and Business Systems', name: 'CSBS', image: 'https://images.unsplash.com/photo-1460925895917-afdab827c52f?auto=format&fit=crop&q=80&w=600' },
|
||||
{ id: 'Artificial Intelligence and Machine Learning (AIML)', name: 'AIML', image: 'https://images.unsplash.com/photo-1677442136019-21780ecad995?auto=format&fit=crop&q=80&w=600' },
|
||||
{ id: 'AIDS', name: 'AIDS', image: 'https://images.unsplash.com/photo-1509228627152-72ae9ae6848d?auto=format&fit=crop&q=80&w=600' },
|
||||
{ id: 'Information Technology', name: 'IT', image: 'https://images.unsplash.com/photo-1498050108023-c5249f4df085?auto=format&fit=crop&q=80&w=600' },
|
||||
{ id: 'ECE', name: 'ECE', image: 'https://images.unsplash.com/photo-1517077304055-6e89abbf09b0?auto=format&fit=crop&q=80&w=600' },
|
||||
{ id: 'Mechanical', name: 'Mechanical', image: 'https://images.unsplash.com/photo-1537462715879-360eeb61a0ad?auto=format&fit=crop&q=80&w=600' },
|
||||
{ id: 'Bio-tech', name: 'Bio-tech', image: 'https://images.unsplash.com/photo-1530210124550-912dc1381cb8?auto=format&fit=crop&q=80&w=600' },
|
||||
{ id: 'CCE', name: 'CCE', image: 'https://images.unsplash.com/photo-1550751827-4bd374c3f58b?auto=format&fit=crop&q=80&w=600' },
|
||||
{ id: 'VLSI', name: 'VLSI', image: 'https://images.unsplash.com/photo-1518770660439-4636190af475?auto=format&fit=crop&q=80&w=600' }
|
||||
],
|
||||
'NON-TECHNICAL': [
|
||||
{ id: 'Management', name: 'Management', image: 'https://images.unsplash.com/photo-1519389950473-47ba0277781c?auto=format&fit=crop&q=80&w=400' },
|
||||
{ id: 'Arts & Culture', name: 'Arts & Culture', image: 'https://images.unsplash.com/photo-1513364776144-60967b0f800f?auto=format&fit=crop&q=80&w=800' },
|
||||
{ id: 'Sports & Athletics', name: 'Sports', image: 'https://images.unsplash.com/photo-1552674605-db6ffd4facb5?auto=format&fit=crop&q=80&w=400' },
|
||||
{ id: 'Social Service', name: 'Social Welfare', image: 'https://images.unsplash.com/photo-1488521787991-ed7bbaae773c?auto=format&fit=crop&q=80&w=400' }
|
||||
],
|
||||
'WORKSHOP': [
|
||||
{ id: 'Software Engineering', name: 'Software Dev', image: 'https://images.unsplash.com/photo-1633356122544-f134324a6cee?auto=format&fit=crop&q=80&w=400' },
|
||||
{ id: 'UI/UX Design', name: 'Design', image: 'https://images.unsplash.com/photo-1561070791-2526d30994b5?auto=format&fit=crop&q=80&w=800' },
|
||||
{ id: 'Cloud', name: 'Cloud/DevOps', image: 'https://images.unsplash.com/photo-1451187580459-43490279c0fa?auto=format&fit=crop&q=80&w=400' }
|
||||
]
|
||||
};
|
||||
|
||||
async function migrate() {
|
||||
for (const [category, domains] of Object.entries(DOMAIN_MAP)) {
|
||||
for (const d of domains) {
|
||||
console.log(`Processing ${d.name}...`);
|
||||
try {
|
||||
const response = await fetch(d.image);
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
const buffer = Buffer.from(arrayBuffer);
|
||||
|
||||
const path = `migrated/${Date.now()}_${d.name.replace(/\\s+/g, '_')}.jpg`;
|
||||
|
||||
const { data: uploadData, error: uploadError } = await supabase.storage
|
||||
.from('domains')
|
||||
.upload(path, buffer, {
|
||||
contentType: 'image/jpeg',
|
||||
upsert: true
|
||||
});
|
||||
|
||||
if (uploadError) {
|
||||
console.log(`Failed to upload ${d.name}`, uploadError);
|
||||
continue;
|
||||
}
|
||||
|
||||
const { data: { publicUrl } } = supabase.storage
|
||||
.from('domains')
|
||||
.getPublicUrl(path);
|
||||
|
||||
console.log(`Uploaded! URL: ${publicUrl}`);
|
||||
|
||||
// Insert into database
|
||||
const { error: insertError } = await supabase.from('domains').insert({
|
||||
name: d.name,
|
||||
description: d.id, // using original id name as description
|
||||
category: category,
|
||||
image: publicUrl,
|
||||
status: 'APPROVED'
|
||||
});
|
||||
|
||||
if (insertError) {
|
||||
console.log(`Failed to insert ${d.name} to DB`, insertError);
|
||||
} else {
|
||||
console.log(`Inserted ${d.name} into DB!`);
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error(`Error processing ${d.name}`, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
migrate().then(() => console.log('Migration complete'));
|
||||
3091
RIT-EVENT-MANAGEMENT--main/package-lock.json
generated
Normal file
3091
RIT-EVENT-MANAGEMENT--main/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
32
RIT-EVENT-MANAGEMENT--main/package.json
Normal file
32
RIT-EVENT-MANAGEMENT--main/package.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "rit-events-hub",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@supabase/supabase-js": "2.39.7",
|
||||
"@types/html2canvas": "^0.5.35",
|
||||
"firebase": "^12.15.0",
|
||||
"html-to-image": "1.11.11",
|
||||
"html2canvas": "^1.4.1",
|
||||
"jspdf": "^4.2.1",
|
||||
"jspdf-autotable": "^5.0.8",
|
||||
"lucide-react": "^0.577.0",
|
||||
"motion": "^12.35.0",
|
||||
"react": "^19.2.3",
|
||||
"react-dom": "^19.2.3",
|
||||
"xlsx-js-style": "^1.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.14.0",
|
||||
"@vitejs/plugin-react": "^5.0.0",
|
||||
"typescript": "~5.8.2",
|
||||
"vite": "^8.0.16"
|
||||
}
|
||||
}
|
||||
BIN
RIT-EVENT-MANAGEMENT--main/public/parchment_texture.png
Normal file
BIN
RIT-EVENT-MANAGEMENT--main/public/parchment_texture.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.0 MiB |
BIN
RIT-EVENT-MANAGEMENT--main/public/wood_roller_texture.png
Normal file
BIN
RIT-EVENT-MANAGEMENT--main/public/wood_roller_texture.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 747 KiB |
576
RIT-EVENT-MANAGEMENT--main/supabase.ts
Normal file
576
RIT-EVENT-MANAGEMENT--main/supabase.ts
Normal file
@@ -0,0 +1,576 @@
|
||||
import { initializeApp, getApp, getApps } from 'firebase/app';
|
||||
import {
|
||||
getAuth,
|
||||
signInWithEmailAndPassword,
|
||||
createUserWithEmailAndPassword,
|
||||
signOut,
|
||||
onAuthStateChanged,
|
||||
getUser
|
||||
} from 'firebase/auth';
|
||||
import {
|
||||
getFirestore,
|
||||
collection,
|
||||
getDocs,
|
||||
doc,
|
||||
getDoc,
|
||||
setDoc,
|
||||
updateDoc,
|
||||
deleteDoc,
|
||||
query,
|
||||
where,
|
||||
orderBy,
|
||||
limit,
|
||||
onSnapshot
|
||||
} from 'firebase/firestore';
|
||||
import { getStorage, ref, uploadBytes, getDownloadURL } from 'firebase/storage';
|
||||
|
||||
// Firebase Configuration from user
|
||||
const firebaseConfig = {
|
||||
apiKey: "AIzaSyBdRUyA7LDtDReUA3TXDys71dSHgD2tOEA",
|
||||
authDomain: "ems-ritchennai1.firebaseapp.com",
|
||||
projectId: "ems-ritchennai1",
|
||||
storageBucket: "ems-ritchennai1.firebasestorage.app",
|
||||
messagingSenderId: "825363154108",
|
||||
appId: "1:825363154108:web:7b6d3430aa3b696fef3cb9",
|
||||
measurementId: "G-3W7ECQ52G1"
|
||||
};
|
||||
|
||||
// Initialize main Firebase App
|
||||
const app = getApps().length === 0 ? initializeApp(firebaseConfig) : getApp();
|
||||
const auth = getAuth(app);
|
||||
const db = getFirestore(app);
|
||||
const storage = getStorage(app);
|
||||
|
||||
/**
|
||||
* Real-time channel handler mapping Supabase real-time channels to Firestore onSnapshot
|
||||
*/
|
||||
class FirestoreChannel {
|
||||
private name: string;
|
||||
private listeners: (() => void)[] = [];
|
||||
|
||||
constructor(name: string) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
on(event: string, filter: { event: string; schema: string; table: string; filter?: string }, callback: () => void) {
|
||||
const table = filter.table;
|
||||
const colRef = collection(db, table);
|
||||
let q: any = colRef;
|
||||
|
||||
if (filter.filter) {
|
||||
const parts = filter.filter.split('=eq.');
|
||||
if (parts.length === 2) {
|
||||
const field = parts[0];
|
||||
const val = parts[1];
|
||||
q = query(colRef, where(field, '==', val));
|
||||
}
|
||||
}
|
||||
|
||||
const unsubscribe = onSnapshot(q, () => {
|
||||
callback();
|
||||
}, (err) => {
|
||||
console.warn(`Firestore Channel ${this.name} snapshot error:`, err);
|
||||
});
|
||||
|
||||
this.listeners.push(unsubscribe);
|
||||
return this;
|
||||
}
|
||||
|
||||
subscribe() {
|
||||
return this;
|
||||
}
|
||||
|
||||
unsubscribeAll() {
|
||||
this.listeners.forEach(unsub => unsub());
|
||||
this.listeners = [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Firestore Query Builder adapter representing Supabase's from('table') syntax
|
||||
*/
|
||||
class FirestoreQueryBuilder {
|
||||
private table: string;
|
||||
private filters: { field: string; op: any; val: any }[] = [];
|
||||
private orderByField: string | null = null;
|
||||
private orderByDirection: 'asc' | 'desc' = 'asc';
|
||||
private limitCount: number | null = null;
|
||||
private isSingle = false;
|
||||
private selectFields = '*';
|
||||
private operation: 'select' | 'insert' | 'update' | 'delete' | 'upsert' = 'select';
|
||||
private payload: any = null;
|
||||
|
||||
constructor(table: string) {
|
||||
this.table = table;
|
||||
}
|
||||
|
||||
select(fields = '*') {
|
||||
this.selectFields = fields;
|
||||
this.operation = 'select';
|
||||
return this;
|
||||
}
|
||||
|
||||
insert(data: any) {
|
||||
this.operation = 'insert';
|
||||
this.payload = data;
|
||||
return this;
|
||||
}
|
||||
|
||||
update(data: any) {
|
||||
this.operation = 'update';
|
||||
this.payload = data;
|
||||
return this;
|
||||
}
|
||||
|
||||
delete() {
|
||||
this.operation = 'delete';
|
||||
return this;
|
||||
}
|
||||
|
||||
upsert(data: any) {
|
||||
this.operation = 'upsert';
|
||||
this.payload = data;
|
||||
return this;
|
||||
}
|
||||
|
||||
eq(field: string, val: any) {
|
||||
this.filters.push({ field, op: '==', val });
|
||||
return this;
|
||||
}
|
||||
|
||||
neq(field: string, val: any) {
|
||||
this.filters.push({ field, op: '!=', val });
|
||||
return this;
|
||||
}
|
||||
|
||||
gt(field: string, val: any) {
|
||||
this.filters.push({ field, op: '>', val });
|
||||
return this;
|
||||
}
|
||||
|
||||
gte(field: string, val: any) {
|
||||
this.filters.push({ field, op: '>=', val });
|
||||
return this;
|
||||
}
|
||||
|
||||
lt(field: string, val: any) {
|
||||
this.filters.push({ field, op: '<', val });
|
||||
return this;
|
||||
}
|
||||
|
||||
lte(field: string, val: any) {
|
||||
this.filters.push({ field, op: '<=', val });
|
||||
return this;
|
||||
}
|
||||
|
||||
in(field: string, val: any[]) {
|
||||
this.filters.push({ field, op: 'in', val });
|
||||
return this;
|
||||
}
|
||||
|
||||
order(field: string, options?: { ascending: boolean }) {
|
||||
this.orderByField = field;
|
||||
this.orderByDirection = options?.ascending === false ? 'desc' : 'asc';
|
||||
return this;
|
||||
}
|
||||
|
||||
limit(count: number) {
|
||||
this.limitCount = count;
|
||||
return this;
|
||||
}
|
||||
|
||||
single() {
|
||||
this.isSingle = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
maybeSingle() {
|
||||
this.isSingle = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
private async fetchDocs(): Promise<any[]> {
|
||||
const colRef = collection(db, this.table);
|
||||
let qConstraints: any[] = [];
|
||||
|
||||
for (const filter of this.filters) {
|
||||
qConstraints.push(where(filter.field, filter.op, filter.val));
|
||||
}
|
||||
|
||||
if (this.orderByField) {
|
||||
qConstraints.push(orderBy(this.orderByField, this.orderByDirection));
|
||||
}
|
||||
|
||||
if (this.limitCount !== null) {
|
||||
qConstraints.push(limit(this.limitCount));
|
||||
}
|
||||
|
||||
const q = query(colRef, ...qConstraints);
|
||||
const snap = await getDocs(q);
|
||||
return snap.docs.map(d => d.data());
|
||||
}
|
||||
|
||||
async execute() {
|
||||
try {
|
||||
if (this.operation === 'insert') {
|
||||
const colRef = collection(db, this.table);
|
||||
const items = Array.isArray(this.payload) ? this.payload : [this.payload];
|
||||
const insertedItems: any[] = [];
|
||||
|
||||
for (const item of items) {
|
||||
const docId = item.id || item.user_id || doc(colRef).id;
|
||||
const docRef = doc(db, this.table, String(docId));
|
||||
const finalItem = { ...item, id: docId };
|
||||
await setDoc(docRef, finalItem);
|
||||
insertedItems.push(finalItem);
|
||||
}
|
||||
|
||||
const data = Array.isArray(this.payload) ? insertedItems : insertedItems[0];
|
||||
return { data: this.isSingle && Array.isArray(data) ? data[0] : data, error: null };
|
||||
}
|
||||
|
||||
if (this.operation === 'update') {
|
||||
const docsToUpdate = await this.fetchDocs();
|
||||
const updatedItems: any[] = [];
|
||||
|
||||
for (const docObj of docsToUpdate) {
|
||||
const docId = docObj.id || docObj.user_id;
|
||||
if (!docId) continue;
|
||||
const docRef = doc(db, this.table, String(docId));
|
||||
await updateDoc(docRef, this.payload);
|
||||
updatedItems.push({ ...docObj, ...this.payload });
|
||||
}
|
||||
|
||||
const data = this.isSingle && updatedItems.length > 0 ? updatedItems[0] : updatedItems;
|
||||
return { data, error: null };
|
||||
}
|
||||
|
||||
if (this.operation === 'delete') {
|
||||
const docsToDelete = await this.fetchDocs();
|
||||
|
||||
for (const docObj of docsToDelete) {
|
||||
const docId = docObj.id || docObj.user_id;
|
||||
if (!docId) continue;
|
||||
const docRef = doc(db, this.table, String(docId));
|
||||
await deleteDoc(docRef);
|
||||
}
|
||||
|
||||
const data = this.isSingle && docsToDelete.length > 0 ? docsToDelete[0] : docsToDelete;
|
||||
return { data, error: null };
|
||||
}
|
||||
|
||||
if (this.operation === 'upsert') {
|
||||
const items = Array.isArray(this.payload) ? this.payload : [this.payload];
|
||||
const result: any[] = [];
|
||||
|
||||
for (const item of items) {
|
||||
const docId = item.id || item.user_id || doc(collection(db, this.table)).id;
|
||||
const docRef = doc(db, this.table, String(docId));
|
||||
const finalItem = { ...item, id: docId };
|
||||
await setDoc(docRef, finalItem, { merge: true });
|
||||
result.push(finalItem);
|
||||
}
|
||||
|
||||
const data = Array.isArray(this.payload) ? result : result[0];
|
||||
return { data: this.isSingle && Array.isArray(data) ? data[0] : data, error: null };
|
||||
}
|
||||
|
||||
// Default: select
|
||||
let list = await this.fetchDocs();
|
||||
|
||||
// Resolve joins if requested. E.g. select('*, events(title, category)')
|
||||
if (this.selectFields && this.selectFields.includes('events(')) {
|
||||
for (const item of list) {
|
||||
if (item.event_id) {
|
||||
const evRef = doc(db, 'events', String(item.event_id));
|
||||
const evSnap = await getDoc(evRef);
|
||||
if (evSnap.exists()) {
|
||||
const evData = evSnap.data();
|
||||
item.events = {
|
||||
title: evData.title,
|
||||
category: evData.category
|
||||
};
|
||||
} else {
|
||||
item.events = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.isSingle) {
|
||||
if (list.length === 0) {
|
||||
return { data: null, error: { message: 'Document not found', code: 'PGRST116' } };
|
||||
}
|
||||
return { data: list[0], error: null };
|
||||
}
|
||||
|
||||
return { data: list, error: null };
|
||||
} catch (err: any) {
|
||||
console.error(`Error in ${this.operation} on ${this.table}:`, err);
|
||||
return { data: null, error: err };
|
||||
}
|
||||
}
|
||||
|
||||
then(onfulfilled?: (value: any) => any, onrejected?: (reason: any) => any) {
|
||||
return this.execute().then(onfulfilled, onrejected);
|
||||
}
|
||||
}
|
||||
|
||||
// Map supabase.auth
|
||||
const supabaseAuth = {
|
||||
async signInWithPassword({ email, password }: any) {
|
||||
try {
|
||||
const credential = await signInWithEmailAndPassword(auth, email, password);
|
||||
const user = {
|
||||
id: credential.user.uid,
|
||||
email: credential.user.email,
|
||||
user_metadata: {}
|
||||
};
|
||||
return { data: { user }, error: null };
|
||||
} catch (error: any) {
|
||||
console.error("Firebase Auth Signin Error:", error);
|
||||
return { data: { user: null }, error };
|
||||
}
|
||||
},
|
||||
|
||||
async signUp({ email, password, options }: any) {
|
||||
try {
|
||||
const credential = await createUserWithEmailAndPassword(auth, email, password);
|
||||
const user = {
|
||||
id: credential.user.uid,
|
||||
email: credential.user.email,
|
||||
user_metadata: options?.data || {}
|
||||
};
|
||||
return { data: { user }, error: null };
|
||||
} catch (error: any) {
|
||||
console.error("Firebase Auth Signup Error:", error);
|
||||
return { data: { user: null }, error };
|
||||
}
|
||||
},
|
||||
|
||||
async signOut() {
|
||||
try {
|
||||
await signOut(auth);
|
||||
return { error: null };
|
||||
} catch (error: any) {
|
||||
return { error };
|
||||
}
|
||||
},
|
||||
|
||||
async getUser() {
|
||||
const currentUser = auth.currentUser;
|
||||
if (currentUser) {
|
||||
return {
|
||||
data: {
|
||||
user: {
|
||||
id: currentUser.uid,
|
||||
email: currentUser.email
|
||||
}
|
||||
},
|
||||
error: null
|
||||
};
|
||||
}
|
||||
return { data: { user: null }, error: null };
|
||||
},
|
||||
|
||||
async getSession() {
|
||||
const currentUser = auth.currentUser;
|
||||
if (currentUser) {
|
||||
return {
|
||||
data: {
|
||||
session: {
|
||||
user: {
|
||||
id: currentUser.uid,
|
||||
email: currentUser.email
|
||||
}
|
||||
}
|
||||
},
|
||||
error: null
|
||||
};
|
||||
}
|
||||
return { data: { session: null }, error: null };
|
||||
},
|
||||
|
||||
onAuthStateChange(callback: (event: string, session: any) => void) {
|
||||
const unsubscribe = onAuthStateChanged(auth, (user) => {
|
||||
if (user) {
|
||||
callback('SIGNED_IN', {
|
||||
user: {
|
||||
id: user.uid,
|
||||
email: user.email
|
||||
}
|
||||
});
|
||||
} else {
|
||||
callback('SIGNED_OUT', null);
|
||||
}
|
||||
});
|
||||
return {
|
||||
data: {
|
||||
subscription: {
|
||||
unsubscribe
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Map active channels
|
||||
const activeChannels = new Map<any, FirestoreChannel>();
|
||||
|
||||
// Expose main supabase client
|
||||
export const supabase = {
|
||||
auth: supabaseAuth,
|
||||
from(table: string) {
|
||||
return new FirestoreQueryBuilder(table);
|
||||
},
|
||||
channel(name: string) {
|
||||
const chan = new FirestoreChannel(name);
|
||||
activeChannels.set(chan, chan);
|
||||
return chan;
|
||||
},
|
||||
removeChannel(chan: any) {
|
||||
if (chan && typeof chan.unsubscribeAll === 'function') {
|
||||
chan.unsubscribeAll();
|
||||
activeChannels.delete(chan);
|
||||
}
|
||||
},
|
||||
storage: {
|
||||
from(bucket: string) {
|
||||
return {
|
||||
upload(path: string, blob: Blob, options?: any) {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const storageRef = ref(storage, `${bucket}/${path}`);
|
||||
const snap = await uploadBytes(storageRef, blob, {
|
||||
contentType: options?.contentType || blob.type
|
||||
});
|
||||
resolve({ data: snap, error: null });
|
||||
} catch (error) {
|
||||
resolve({ data: null, error });
|
||||
}
|
||||
});
|
||||
},
|
||||
getPublicUrl(path: string) {
|
||||
const storageRef = ref(storage, `${bucket}/${path}`);
|
||||
return {
|
||||
data: {
|
||||
publicUrl: `https://firebasestorage.googleapis.com/v0/b/${firebaseConfig.storageBucket}/o/${encodeURIComponent(`${bucket}/${path}`)}?alt=media`
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a secondary App instance for administrative user signups.
|
||||
* This prevents the administrator's session from being overwritten.
|
||||
*/
|
||||
export const createAdminClient = () => {
|
||||
const adminAppName = 'AdminAuthApp';
|
||||
let adminApp;
|
||||
if (getApps().some(app => app.name === adminAppName)) {
|
||||
adminApp = getApp(adminAppName);
|
||||
} else {
|
||||
adminApp = initializeApp(firebaseConfig, adminAppName);
|
||||
}
|
||||
const adminAuth = getAuth(adminApp);
|
||||
|
||||
return {
|
||||
auth: {
|
||||
async signUp({ email, password }: any) {
|
||||
try {
|
||||
const credential = await createUserWithEmailAndPassword(adminAuth, email, password);
|
||||
// Immediately sign out from the admin app so we don't store session
|
||||
await signOut(adminAuth);
|
||||
return { data: { user: { id: credential.user.uid, email: credential.user.email } }, error: null };
|
||||
} catch (error: any) {
|
||||
console.error("Firebase Admin Auth Signup Error:", error);
|
||||
return { data: { user: null }, error };
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Base64 upload helper mapping to Firebase Storage
|
||||
*/
|
||||
export const uploadToSupabase = async (
|
||||
base64String: string,
|
||||
path: string,
|
||||
bucket: string = 'rit-events'
|
||||
): Promise<string> => {
|
||||
try {
|
||||
const parts = base64String.split(';base64,');
|
||||
if (parts.length < 2) throw new Error('Invalid base64 string');
|
||||
|
||||
const contentType = parts[0].split(':')[1];
|
||||
const raw = window.atob(parts[1]);
|
||||
const rawLength = raw.length;
|
||||
const uInt8Array = new Uint8Array(rawLength);
|
||||
|
||||
for (let i = 0; i < rawLength; ++i) {
|
||||
uInt8Array[i] = raw.charCodeAt(i);
|
||||
}
|
||||
|
||||
const blob = new Blob([uInt8Array], { type: contentType });
|
||||
const storageRef = ref(storage, `${bucket}/${path}`);
|
||||
await uploadBytes(storageRef, blob, { contentType });
|
||||
const publicUrl = await getDownloadURL(storageRef);
|
||||
return publicUrl;
|
||||
} catch (error) {
|
||||
console.error('Firebase Storage Upload Error:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const uploadImageToSupabase = uploadToSupabase;
|
||||
|
||||
// Automatic Seeder for domains if empty
|
||||
(async () => {
|
||||
try {
|
||||
const colRef = collection(db, 'domains');
|
||||
const snap = await getDocs(colRef);
|
||||
if (snap.empty) {
|
||||
console.log('Domains collection is empty. Seeding initial domains...');
|
||||
const DOMAIN_MAP = {
|
||||
'TECHNICAL': [
|
||||
{ name: 'CSE', description: 'Computer Science and Engineering', image: 'https://images.unsplash.com/photo-1517694712202-14dd9538aa97?auto=format&fit=crop&q=80&w=600', status: 'APPROVED', category: 'TECHNICAL' },
|
||||
{ name: 'CSBS', description: 'Computer Science and Business Systems', image: 'https://images.unsplash.com/photo-1460925895917-afdab827c52f?auto=format&fit=crop&q=80&w=600', status: 'APPROVED', category: 'TECHNICAL' },
|
||||
{ name: 'AIML', description: 'Artificial Intelligence and Machine Learning (AIML)', image: 'https://images.unsplash.com/photo-1677442136019-21780ecad995?auto=format&fit=crop&q=80&w=600', status: 'APPROVED', category: 'TECHNICAL' },
|
||||
{ name: 'AIDS', description: 'AIDS', image: 'https://images.unsplash.com/photo-1509228627152-72ae9ae6848d?auto=format&fit=crop&q=80&w=600', status: 'APPROVED', category: 'TECHNICAL' },
|
||||
{ name: 'IT', description: 'Information Technology', image: 'https://images.unsplash.com/photo-1498050108023-c5249f4df085?auto=format&fit=crop&q=80&w=600', status: 'APPROVED', category: 'TECHNICAL' },
|
||||
{ name: 'ECE', description: 'ECE', image: 'https://images.unsplash.com/photo-1517077304055-6e89abbf09b0?auto=format&fit=crop&q=80&w=600', status: 'APPROVED', category: 'TECHNICAL' },
|
||||
{ name: 'Mechanical', description: 'Mechanical', image: 'https://images.unsplash.com/photo-1537462715879-360eeb61a0ad?auto=format&fit=crop&q=80&w=600', status: 'APPROVED', category: 'TECHNICAL' },
|
||||
{ name: 'Bio-tech', description: 'Bio-tech', image: 'https://images.unsplash.com/photo-1530210124550-912dc1381cb8?auto=format&fit=crop&q=80&w=600', status: 'APPROVED', category: 'TECHNICAL' },
|
||||
{ name: 'CCE', description: 'CCE', image: 'https://images.unsplash.com/photo-1550751827-4bd374c3f58b?auto=format&fit=crop&q=80&w=600', status: 'APPROVED', category: 'TECHNICAL' },
|
||||
{ name: 'VLSI', description: 'VLSI', image: 'https://images.unsplash.com/photo-1518770660439-4636190af475?auto=format&fit=crop&q=80&w=600', status: 'APPROVED', category: 'TECHNICAL' }
|
||||
],
|
||||
'NON-TECHNICAL': [
|
||||
{ name: 'Management', description: 'Management', image: 'https://images.unsplash.com/photo-1519389950473-47ba0277781c?auto=format&fit=crop&q=80&w=400', status: 'APPROVED', category: 'NON-TECHNICAL' },
|
||||
{ name: 'Arts & Culture', description: 'Arts & Culture', image: 'https://images.unsplash.com/photo-1513364776144-60967b0f800f?auto=format&fit=crop&q=80&w=800', status: 'APPROVED', category: 'NON-TECHNICAL' },
|
||||
{ name: 'Sports', description: 'Sports & Athletics', image: 'https://images.unsplash.com/photo-1552674605-db6ffd4facb5?auto=format&fit=crop&q=80&w=400', status: 'APPROVED', category: 'NON-TECHNICAL' },
|
||||
{ name: 'Social Welfare', description: 'Social Service', image: 'https://images.unsplash.com/photo-1488521787991-ed7bbaae773c?auto=format&fit=crop&q=80&w=400', status: 'APPROVED', category: 'NON-TECHNICAL' }
|
||||
],
|
||||
'WORKSHOP': [
|
||||
{ name: 'Software Dev', description: 'Software Engineering', image: 'https://images.unsplash.com/photo-1633356122544-f134324a6cee?auto=format&fit=crop&q=80&w=400', status: 'APPROVED', category: 'WORKSHOP' },
|
||||
{ name: 'Design', description: 'UI/UX Design', image: 'https://images.unsplash.com/photo-1561070791-2526d30994b5?auto=format&fit=crop&q=80&w=800', status: 'APPROVED', category: 'WORKSHOP' },
|
||||
{ name: 'Cloud/DevOps', description: 'Cloud', image: 'https://images.unsplash.com/photo-1451187580459-43490279c0fa?auto=format&fit=crop&q=80&w=400', status: 'APPROVED', category: 'WORKSHOP' }
|
||||
]
|
||||
};
|
||||
|
||||
for (const [_, domains] of Object.entries(DOMAIN_MAP)) {
|
||||
for (const domain of domains) {
|
||||
const docId = doc(colRef).id;
|
||||
const docRef = doc(db, 'domains', docId);
|
||||
await setDoc(docRef, { ...domain, id: docId });
|
||||
}
|
||||
}
|
||||
console.log('Seeded domains successfully.');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Automatic domains seeder failed:', err);
|
||||
}
|
||||
})();
|
||||
29
RIT-EVENT-MANAGEMENT--main/tsconfig.json
Normal file
29
RIT-EVENT-MANAGEMENT--main/tsconfig.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"experimentalDecorators": true,
|
||||
"useDefineForClassFields": false,
|
||||
"module": "ESNext",
|
||||
"lib": [
|
||||
"ES2022",
|
||||
"DOM",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"skipLibCheck": true,
|
||||
"types": [
|
||||
"node"
|
||||
],
|
||||
"moduleResolution": "bundler",
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"allowJs": true,
|
||||
"jsx": "react-jsx",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./*"
|
||||
]
|
||||
},
|
||||
"allowImportingTsExtensions": true,
|
||||
"noEmit": true
|
||||
}
|
||||
}
|
||||
151
RIT-EVENT-MANAGEMENT--main/types.ts
Normal file
151
RIT-EVENT-MANAGEMENT--main/types.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
|
||||
export type AppState = 'WELCOME' | 'LOGIN' | 'DASHBOARD' | 'ADMIN_LANDING' | 'FACULTY_DASHBOARD' | 'VERIFY';
|
||||
export type UserRole = 'STUDENT' | 'COORDINATOR' | 'ADMIN';
|
||||
export type DashboardView = 'HOME' | 'EVENTS' | 'REGISTRATIONS' | 'PROFILE' | 'ABOUT' | 'CONTACT' | 'STATUS_TRACKER';
|
||||
export type FacultyView = 'OVERVIEW' | 'PARTICIPANTS' | 'ATTENDANCE' | 'NOTIFICATIONS' | 'PROFILE';
|
||||
|
||||
export interface ResourcePerson {
|
||||
id?: string;
|
||||
type: 'INTERNAL' | 'EXTERNAL';
|
||||
name: string;
|
||||
dept?: string;
|
||||
college_name?: string;
|
||||
phone: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
export interface Batch {
|
||||
id: number;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
date?: string;
|
||||
resourcePerson?: ResourcePerson;
|
||||
}
|
||||
|
||||
export interface EventDay {
|
||||
date: string;
|
||||
batches: Batch[]; // If unified, this will have one batch at index 0
|
||||
startTime?: string;
|
||||
endTime?: string;
|
||||
resourcePerson?: ResourcePerson;
|
||||
}
|
||||
|
||||
export interface EventSchedule {
|
||||
id: string;
|
||||
event_id: string;
|
||||
day_idx: number;
|
||||
batch_idx: number;
|
||||
date: string;
|
||||
start_time: string;
|
||||
end_time: string;
|
||||
resource_person?: ResourcePerson;
|
||||
}
|
||||
|
||||
export interface Event {
|
||||
id: string;
|
||||
title: string;
|
||||
date: string;
|
||||
location: string;
|
||||
category: 'TECHNICAL' | 'NON-TECHNICAL' | 'WORKSHOP' | 'CENTRE-ACTIVITY';
|
||||
domain: string;
|
||||
club?: string;
|
||||
image: string;
|
||||
pricingType: 'PAID' | 'FREE';
|
||||
coordinator: string;
|
||||
status?: 'Registered' | 'Event Ongoing' | 'Completed' | 'Scheduled';
|
||||
registrationDeadline?: string;
|
||||
maxParticipants?: number;
|
||||
durationDays?: number;
|
||||
schedule?: EventSchedule[];
|
||||
deptLimits?: Record<string, number>; // Maps department codes to specific seat counts
|
||||
deptSectionLimits?: Record<string, Record<string, number>>; // Maps department codes to their section capacities
|
||||
currentParticipants?: number;
|
||||
currentDeptCounts?: Record<string, number>;
|
||||
currentDeptSectionCounts?: Record<string, Record<string, number>>; // Maps department codes to their sections' current counts
|
||||
event_summary?: string;
|
||||
isTeamEvent?: boolean;
|
||||
teamSizeLimit?: number;
|
||||
teamComposition?: 'INTER_DEPT' | 'MIXED';
|
||||
created_by?: string;
|
||||
participantType?: 'INTERNAL' | 'EXTERNAL' | 'BOTH';
|
||||
verificationStatus?: 'PENDING' | 'PENDING_HOD' | 'PENDING_ADMIN' | 'APPROVED' | 'REJECTED';
|
||||
refreshment_expense?: number;
|
||||
transportation_expense?: number;
|
||||
session_coverage_fee?: number;
|
||||
total_expense?: number;
|
||||
conducting_dept?: string;
|
||||
request_by_faculty?: string;
|
||||
request_by_HOD?: string;
|
||||
}
|
||||
|
||||
export interface Announcement {
|
||||
id: string;
|
||||
title: string;
|
||||
message: string;
|
||||
timestamp: any;
|
||||
expiresAt?: any;
|
||||
type: 'DELAY' | 'INFO' | 'URGENT' | 'ENDED' | 'ONGOING';
|
||||
eventId?: string;
|
||||
eventTitle?: string;
|
||||
}
|
||||
|
||||
export interface Ticket {
|
||||
ticketId: string;
|
||||
qrCodeData: string;
|
||||
eventId: string;
|
||||
userId: string;
|
||||
userEmail: string;
|
||||
userName: string;
|
||||
regNo?: string;
|
||||
dept?: string;
|
||||
section?: string;
|
||||
status: 'ACTIVE' | 'USED' | 'CANCELLED';
|
||||
createdAt: any;
|
||||
}
|
||||
|
||||
export interface Participant {
|
||||
id: string;
|
||||
studentName: string;
|
||||
regNo: string;
|
||||
branch: string;
|
||||
year: string;
|
||||
eventName: string;
|
||||
eventId: string;
|
||||
location: string;
|
||||
timings: string;
|
||||
category: 'TECHNICAL' | 'NON-TECHNICAL' | 'WORKSHOP' | 'CENTRE-ACTIVITY';
|
||||
}
|
||||
|
||||
export interface StudentRequest {
|
||||
id: string;
|
||||
studentName: string;
|
||||
rollNo: string;
|
||||
branch: string;
|
||||
eventName: string;
|
||||
eventId: string;
|
||||
timestamp: string;
|
||||
status: 'PENDING' | 'APPROVED' | 'REJECTED';
|
||||
}
|
||||
|
||||
|
||||
export interface UserProfile {
|
||||
name: string;
|
||||
email: string;
|
||||
phone?: string;
|
||||
year?: string;
|
||||
section?: string;
|
||||
department?: string;
|
||||
reg_no?: string;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
export interface SpecialEvent {
|
||||
id: string;
|
||||
created_at: string;
|
||||
title: string;
|
||||
description: string;
|
||||
link: string;
|
||||
created_by: string;
|
||||
is_active: boolean;
|
||||
verificationStatus?: 'PENDING' | 'PENDING_HOD' | 'PENDING_ADMIN' | 'APPROVED' | 'REJECTED';
|
||||
}
|
||||
205
RIT-EVENT-MANAGEMENT--main/utils/pdfGenerator.ts
Normal file
205
RIT-EVENT-MANAGEMENT--main/utils/pdfGenerator.ts
Normal file
@@ -0,0 +1,205 @@
|
||||
import { jsPDF } from 'jspdf';
|
||||
import autoTable from 'jspdf-autotable';
|
||||
import { Event } from '../types';
|
||||
|
||||
export const generateEventDetailsPDF = (event: Event) => {
|
||||
const doc = new jsPDF();
|
||||
|
||||
// Set fonts and colors
|
||||
doc.setFont('helvetica');
|
||||
|
||||
// --- Header Section ---
|
||||
doc.setFillColor(0, 74, 153); // #004a99
|
||||
doc.rect(0, 0, 210, 40, 'F');
|
||||
|
||||
doc.setTextColor(255, 255, 255);
|
||||
doc.setFontSize(22);
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.text('RAJALAKSHMI INSTITUTE OF TECHNOLOGY', 105, 18, { align: 'center' });
|
||||
|
||||
doc.setFontSize(14);
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.text('OFFICIAL EVENT PROPOSAL & VERIFICATION DOCUMENT', 105, 28, { align: 'center' });
|
||||
|
||||
// --- Basic Information ---
|
||||
let startY = 50;
|
||||
doc.setTextColor(0, 0, 0);
|
||||
|
||||
const addSectionTitle = (title: string, yPos: number) => {
|
||||
doc.setFontSize(14);
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.setTextColor(0, 74, 153);
|
||||
doc.text(title, 14, yPos);
|
||||
doc.setDrawColor(0, 74, 153);
|
||||
doc.setLineWidth(0.5);
|
||||
doc.line(14, yPos + 2, 196, yPos + 2);
|
||||
doc.setTextColor(0, 0, 0);
|
||||
return yPos + 10;
|
||||
};
|
||||
|
||||
startY = addSectionTitle('1. General Overview', startY);
|
||||
|
||||
doc.setFontSize(10);
|
||||
doc.setFont('helvetica', 'bold');
|
||||
|
||||
const basicInfo = [
|
||||
['Event Title:', event.title.toUpperCase()],
|
||||
['Category & Domain:', `${event.category} - ${event.domain || 'N/A'}`],
|
||||
['Conducting Dept:', event.conducting_dept || 'General'],
|
||||
['Coordinator:', event.coordinator || 'N/A'],
|
||||
['Club / Entity:', event.club || 'N/A'],
|
||||
['Participant Type:', event.participantType || 'Internal'],
|
||||
['Creation Date:', event.request_by_faculty ? new Date(event.request_by_faculty).toLocaleString() : 'N/A']
|
||||
];
|
||||
|
||||
basicInfo.forEach((info, idx) => {
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.text(info[0], 14, startY + (idx * 7));
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.text(info[1], 55, startY + (idx * 7));
|
||||
});
|
||||
|
||||
startY += (basicInfo.length * 7) + 10;
|
||||
|
||||
// --- Operational Logistics ---
|
||||
startY = addSectionTitle('2. Operational Logistics', startY);
|
||||
|
||||
const eventTime = event.schedule && event.schedule.length > 0 ? `${event.schedule[0].start_time} - ${event.schedule[0].end_time}` : 'N/A';
|
||||
const logisticsInfo = [
|
||||
['Date & Time:', `${event.date} at ${eventTime}`],
|
||||
['Duration:', `${event.durationDays || 1} Day(s)`],
|
||||
['Venue:', event.location || 'N/A'],
|
||||
['Event Format:', event.isTeamEvent ? `Team Play (Max ${event.teamSizeLimit || 1} members)` : 'Solo Participation'],
|
||||
['Registration Deadline:', event.registrationDeadline ? new Date(event.registrationDeadline).toLocaleString() : 'N/A'],
|
||||
['Global Capacity:', event.maxParticipants ? `${event.maxParticipants} Seats` : 'Unlimited']
|
||||
];
|
||||
|
||||
logisticsInfo.forEach((info, idx) => {
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.text(info[0], 14, startY + (idx * 7));
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.text(info[1], 55, startY + (idx * 7));
|
||||
});
|
||||
|
||||
startY += (logisticsInfo.length * 7) + 10;
|
||||
|
||||
// --- Detailed Summary ---
|
||||
startY = addSectionTitle('3. Detailed Summary', startY);
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.setFontSize(10);
|
||||
|
||||
const splitSummary = doc.splitTextToSize(event.event_summary || 'No detailed summary provided for this event.', 180);
|
||||
doc.text(splitSummary, 14, startY);
|
||||
|
||||
startY += (splitSummary.length * 5) + 10;
|
||||
|
||||
// --- Schedule & Resource Persons ---
|
||||
if (startY > 230) {
|
||||
doc.addPage();
|
||||
startY = 20;
|
||||
}
|
||||
|
||||
if (event.schedule && event.schedule.length > 0) {
|
||||
startY = addSectionTitle('4. Schedule & Resource Persons', startY);
|
||||
|
||||
const scheduleData = event.schedule.map(slot => [
|
||||
`Day ${slot.day_idx} - Batch ${slot.batch_idx}`,
|
||||
`${slot.date} (${slot.start_time} - ${slot.end_time})`,
|
||||
slot.resource_person ? `${slot.resource_person.name}\n${slot.resource_person.dept || ''} ${slot.resource_person.college_name || ''}` : 'N/A',
|
||||
slot.resource_person ? `${slot.resource_person.email}\n${slot.resource_person.phone}` : 'N/A'
|
||||
]);
|
||||
|
||||
autoTable(doc, {
|
||||
startY: startY,
|
||||
head: [['Session', 'Timing', 'Resource Person', 'Contact']],
|
||||
body: scheduleData,
|
||||
theme: 'grid',
|
||||
headStyles: { fillColor: [0, 74, 153] },
|
||||
styles: { fontSize: 9 }
|
||||
});
|
||||
|
||||
startY = (doc as any).lastAutoTable.finalY + 15;
|
||||
}
|
||||
|
||||
// --- Department Quotas ---
|
||||
if (startY > 230) {
|
||||
doc.addPage();
|
||||
startY = 20;
|
||||
}
|
||||
|
||||
if (event.deptLimits && Object.keys(event.deptLimits).length > 0) {
|
||||
startY = addSectionTitle('5. Department Seat Quotas', startY);
|
||||
|
||||
const quotaData = Object.entries(event.deptLimits).map(([dept, maxSeats]) => {
|
||||
const current = event.currentDeptCounts?.[dept] || 0;
|
||||
return [dept, `${current} Enrolled`, `${maxSeats} Limit`];
|
||||
});
|
||||
|
||||
autoTable(doc, {
|
||||
startY: startY,
|
||||
head: [['Department', 'Current Enrollment', 'Max Seats Allocated']],
|
||||
body: quotaData,
|
||||
theme: 'grid',
|
||||
headStyles: { fillColor: [249, 115, 22] }, // Orange theme for quotas
|
||||
styles: { fontSize: 9 }
|
||||
});
|
||||
|
||||
startY = (doc as any).lastAutoTable.finalY + 15;
|
||||
}
|
||||
|
||||
// --- Financial Projections ---
|
||||
if (startY > 230) {
|
||||
doc.addPage();
|
||||
startY = 20;
|
||||
}
|
||||
|
||||
startY = addSectionTitle('6. Financial Projections', startY);
|
||||
|
||||
const financeData = [
|
||||
['Refreshment Expenses', `Rs. ${event.refreshment_expense || 0}`],
|
||||
['Transportation Expenses', `Rs. ${event.transportation_expense || 0}`],
|
||||
['Session Coverage Fees', `Rs. ${(event as any).session_coverage_fee || 0}`],
|
||||
['Total Projected Budget', `Rs. ${event.total_expense || 0}`]
|
||||
];
|
||||
|
||||
autoTable(doc, {
|
||||
startY: startY,
|
||||
body: financeData,
|
||||
theme: 'plain',
|
||||
styles: { fontSize: 10, cellPadding: 3 },
|
||||
columnStyles: { 0: { fontStyle: 'bold' }, 1: { halign: 'right' } },
|
||||
didParseCell: function(data) {
|
||||
if (data.row.index === 3) {
|
||||
data.cell.styles.fontStyle = 'bold';
|
||||
data.cell.styles.textColor = [16, 185, 129]; // Emerald 500
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
startY = (doc as any).lastAutoTable.finalY + 15;
|
||||
|
||||
// --- Footer / Verification Box ---
|
||||
if (startY > 250) {
|
||||
doc.addPage();
|
||||
startY = 20;
|
||||
}
|
||||
|
||||
doc.setDrawColor(0, 0, 0);
|
||||
doc.setLineWidth(0.2);
|
||||
doc.rect(14, startY, 182, 35);
|
||||
|
||||
doc.setFontSize(9);
|
||||
doc.setFont('helvetica', 'italic');
|
||||
doc.text('This is a system generated event verification document.', 105, startY + 6, { align: 'center' });
|
||||
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.setFontSize(10);
|
||||
doc.text(`Status: ${event.verificationStatus || 'PENDING'}`, 20, startY + 18);
|
||||
|
||||
doc.setFontSize(8);
|
||||
doc.text(`Generated on: ${new Date().toLocaleString()}`, 20, startY + 28);
|
||||
doc.text('Rajalakshmi Institute of Technology - Events Hub', 130, startY + 28);
|
||||
|
||||
// Save PDF
|
||||
doc.save(`${event.title.replace(/\s+/g, '_')}_Verification_Report.pdf`);
|
||||
};
|
||||
8
RIT-EVENT-MANAGEMENT--main/vercel.json
Normal file
8
RIT-EVENT-MANAGEMENT--main/vercel.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"rewrites": [
|
||||
{
|
||||
"source": "/(.*)",
|
||||
"destination": "/index.html"
|
||||
}
|
||||
]
|
||||
}
|
||||
24
RIT-EVENT-MANAGEMENT--main/vite.config.ts
Normal file
24
RIT-EVENT-MANAGEMENT--main/vite.config.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import path from 'path';
|
||||
import { defineConfig, loadEnv } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig(({ mode }) => {
|
||||
const env = loadEnv(mode, '.', '');
|
||||
return {
|
||||
server: {
|
||||
port: 3006,
|
||||
strictPort: false,
|
||||
host: '0.0.0.0',
|
||||
},
|
||||
plugins: [react()],
|
||||
define: {
|
||||
'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY),
|
||||
'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY)
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, '.'),
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user