diff --git a/.codacy/codacy.yaml b/.codacy/codacy.yaml new file mode 100644 index 0000000..15365c7 --- /dev/null +++ b/.codacy/codacy.yaml @@ -0,0 +1,15 @@ +runtimes: + - dart@3.7.2 + - go@1.22.3 + - java@17.0.10 + - node@22.2.0 + - python@3.11.11 +tools: + - dartanalyzer@3.7.2 + - eslint@8.57.0 + - lizard@1.17.31 + - pmd@7.11.0 + - pylint@3.3.6 + - revive@1.7.0 + - semgrep@1.78.0 + - trivy@0.66.0 diff --git a/app/actions/auth/auth.ts b/app/actions/auth/auth.ts new file mode 100644 index 0000000..af4ae43 --- /dev/null +++ b/app/actions/auth/auth.ts @@ -0,0 +1,62 @@ +import {supabaseAdmin} from '@/lib/supabase-admin'; +import { cookies } from 'next/headers'; +import jwt from 'jsonwebtoken'; + +export default async function auth(profile:any){ + console.log(profile); + //check if user already exists + const { data:existingUser, error:selectError} = await supabaseAdmin + .from('users') + .select('email') + .eq('email',profile.email) + .single(); + + if(selectError && selectError.code !== 'PGRST116'){ + console.error('DB Select error:', selectError); + return { error: 'Database error', status: 500}; + } + + if(existingUser){ + return {status: 'ok',user:existingUser}; + } + + const dep:string = profile.email.split('@')[1].split('.')[0]; + const match = profile.email.match(/_?b(\d+)@/); + let year = match ? parseInt(match[1], 10) + 2000 : null; + + // Insert Entry into tabble + const {data:newUser,error:insertError} = await supabaseAdmin + .from('users') + .insert({ + email:profile.email, + name:profile.name, + picture:profile.picture, + branch:dep, + year:year, + created_at:new Date().toISOString(), + }) + .select('uid,name,email,picture,created_at,branch,year,created_at') + .single(); + + if (insertError) { + console.error('Insert error:', insertError); + return { + error: 'Failed to create user', + status: 500 + }; + } + const token = jwt.sign( + { userId: newUser.uid, email: newUser.email,name: newUser.name }, + process.env.JWT_SECRET!, + { expiresIn: '7d' } + ); + + const cookieStore = await cookies(); + cookieStore.set('token', token, { + httpOnly: false, + secure: process.env.NODE_ENV === 'production', + sameSite: 'strict', + maxAge: 60 * 60 * 24 * 7 // 7 days + }); + return {status: 'ok',user:newUser}; +} \ No newline at end of file diff --git a/app/actions/events.ts b/app/actions/events.ts new file mode 100644 index 0000000..99270d5 --- /dev/null +++ b/app/actions/events.ts @@ -0,0 +1,441 @@ +'use server' + +import { getServerSession } from "next-auth"; +import { authOptions } from "@/lib/auth"; +import { supabaseAdmin } from "@/lib/supabase-admin"; +import { revalidatePath } from "next/cache"; + +type ActionResult = + | { success: true; data: T } + | { success: false; error: string }; + +// Helper function to calculate event status based on date +function calculateEventStatus(eventDate: string | null, currentStatus: string): string { + // Don't change cancelled events + if (currentStatus === 'cancelled') return 'cancelled'; + + if (!eventDate) return currentStatus || 'upcoming'; + + const today = new Date(); + today.setHours(0, 0, 0, 0); + + const event = new Date(eventDate); + event.setHours(0, 0, 0, 0); + + const diffTime = event.getTime() - today.getTime(); + const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); + + if (diffDays > 0) return 'upcoming'; + if (diffDays === 0) return 'ongoing'; + return 'completed'; +} + +// Helper function to calculate registration status based on deadline, event date, and capacity +function calculateRegistrationStatus( + eventDate: string | null, + registrationDeadline: string | null, + currentStatus: string, + participantCount: number, + maxParticipants: number +): string { + const today = new Date(); + today.setHours(0, 0, 0, 0); + + // Check if event has already passed + if (eventDate) { + const event = new Date(eventDate); + event.setHours(0, 0, 0, 0); + if (event < today) return 'closed'; + } + + // Check if registration deadline has passed + if (registrationDeadline) { + const deadline = new Date(registrationDeadline); + deadline.setHours(23, 59, 59, 999); // End of deadline day + if (new Date() > deadline) return 'closed'; + } + + // Check if capacity is full + if (maxParticipants && participantCount >= maxParticipants) return 'closed'; + + // Check if registration deadline is in the future (registration is open) + if (registrationDeadline) { + const deadline = new Date(registrationDeadline); + deadline.setHours(0, 0, 0, 0); + if (deadline >= today) return 'open'; + } + + // If no deadline set but event is in the future, consider it open + if (eventDate) { + const event = new Date(eventDate); + event.setHours(0, 0, 0, 0); + if (event >= today) return 'open'; + } + + return currentStatus || 'upcoming'; +} + +// Helper function to auto-update event statuses in the database +async function autoUpdateEventStatuses(events: any[]): Promise { + const updates: { id: string; event_status?: string; registrationstatus?: string }[] = []; + + const updatedEvents = events.map((event) => { + const calculatedEventStatus = calculateEventStatus(event.date, event.event_status); + const calculatedRegStatus = calculateRegistrationStatus( + event.date, + event.registration_deadline, + event.registrationstatus, + event.participantcount || 0, + event.maxparticipants || 0 + ); + + const needsUpdate = + calculatedEventStatus !== event.event_status || + calculatedRegStatus !== event.registrationstatus; + + if (needsUpdate) { + updates.push({ + id: event.id, + event_status: calculatedEventStatus, + registrationstatus: calculatedRegStatus + }); + return { + ...event, + event_status: calculatedEventStatus, + registrationstatus: calculatedRegStatus + }; + } + return event; + }); + + // Batch update events that need status changes + for (const update of updates) { + await supabaseAdmin + .from("events") + .update({ + event_status: update.event_status, + registrationstatus: update.registrationstatus, + updated_at: new Date().toISOString() + }) + .eq("id", update.id); + } + + return updatedEvents; +} + +// GET all events +export async function getEvents(): Promise> { + try { + const session = await getServerSession(authOptions); + + if (!session?.user?.email) { + return { success: false, error: "Unauthorized" }; + } + + const { data: events, error } = await supabaseAdmin + .from("events") + .select("*") + .order("created_at", { ascending: false }); + + if (error) { + console.error("Error fetching events:", error); + return { success: false, error: "Failed to fetch events" }; + } + + // Auto-update event statuses based on date + const updatedEvents = await autoUpdateEventStatuses(events); + + return { success: true, data: updatedEvents }; + } catch (error: any) { + console.error("Error in getEvents:", error); + return { success: false, error: "Internal server error" }; + } +} + +// CREATE a new event (Admin only) +export async function createEvent(eventData: { + title: string; + description: string; + date?: string; + time?: string; + location: string; + maxParticipants?: number; + registrationstatus?: string; + registrationDeadline?: string; + category?: string; + organizer?: string; + tags?: string[]; + requirements?: string[]; + imageUrl?: string; + teamEvent?: boolean; + maxTeamSize?: number; + minTeamSize?: number; + eventStatus?: string; + isFeatured?: boolean; + externalLink?: string; + eventHighlights?: any[]; + eventPhotos?: string[]; + attendanceCount?: number; +}): Promise { + try { + const session = await getServerSession(authOptions); + + if (!session?.user?.email) { + return { success: false, error: "Unauthorized" }; + } + + // Check if user is admin + const { data: userData } = await supabaseAdmin + .from("users") + .select("is_admin") + .eq("email", session.user.email) + .single(); + + if (userData?.is_admin !== 1) { + return { success: false, error: "Forbidden - Admin access required" }; + } + + // Validate required fields + if (!eventData.title || !eventData.description || !eventData.location) { + return { success: false, error: "Missing required fields: title, description, location" }; + } + + // Default placeholder image if not provided + const DEFAULT_IMAGE = 'https://images.unsplash.com/photo-1540575467063-178a50c2df87?w=800&h=400&fit=crop'; + + const { data: event, error } = await supabaseAdmin + .from("events") + .insert([ + { + title: eventData.title, + description: eventData.description, + date: eventData.date || new Date().toISOString().split('T')[0], + time: eventData.time, + location: eventData.location, + maxparticipants: eventData.maxParticipants || 100, + registrationstatus: eventData.registrationstatus || 'open', + registration_deadline: eventData.registrationDeadline || null, + category: eventData.category || "workshop", + organizer: eventData.organizer || "COC", + tags: eventData.tags || [], + requirements: eventData.requirements || [], + imageurl: eventData.imageUrl || DEFAULT_IMAGE, + participantcount: 0, + team_event: eventData.teamEvent ?? false, + max_team_size: eventData.maxTeamSize || 1, + min_team_size: eventData.minTeamSize || 1, + event_status: eventData.eventStatus || 'upcoming', + is_featured: eventData.isFeatured ?? false, + external_link: eventData.externalLink, + event_highlights: eventData.eventHighlights || [], + event_photos: eventData.eventPhotos || [], + attendance_count: eventData.attendanceCount || 0, + }, + ]) + .select() + .single(); + + if (error) { + console.error("Error creating event:", error); + return { success: false, error: error.message || "Failed to create event" }; + } + + // Revalidate pages that show events + revalidatePath("/dashboard/events"); + revalidatePath("/admin-dashboard"); + revalidatePath("/dashboard"); + + return { success: true, data: event }; + } catch (error: any) { + console.error("Error in createEvent:", error); + return { success: false, error: "Internal server error" }; + } +} + +// UPDATE an event (Admin only) +export async function updateEvent(eventId: string, updates: Partial): Promise { + try { + const session = await getServerSession(authOptions); + + if (!session?.user?.email) { + return { success: false, error: "Unauthorized" }; + } + + // Check if user is admin + const { data: userData } = await supabaseAdmin + .from("users") + .select("is_admin") + .eq("email", session.user.email) + .single(); + + if (userData?.is_admin !== 1) { + return { success: false, error: "Forbidden - Admin access required" }; + } + + // Transform camelCase keys to snake_case for database columns + const dbUpdates: Record = {}; + const keyMap: Record = { + maxParticipants: 'maxparticipants', + imageUrl: 'imageurl', + participantCount: 'participantcount', + teamEvent: 'team_event', + maxTeamSize: 'max_team_size', + minTeamSize: 'min_team_size', + eventStatus: 'event_status', + isFeatured: 'is_featured', + externalLink: 'external_link', + eventHighlights: 'event_highlights', + eventPhotos: 'event_photos', + attendanceCount: 'attendance_count', + createdAt: 'created_at', + updatedAt: 'updated_at', + registrationDeadline: 'registration_deadline', + }; + + for (const [key, value] of Object.entries(updates)) { + // Skip undefined values and empty strings for optional fields + if (value === undefined) continue; + // Skip createdAt - it should not be updated + if (key === 'createdAt' || key === 'created_at') continue; + + const dbKey = keyMap[key] || key; + dbUpdates[dbKey] = value; + } + + // Always update the updated_at timestamp + dbUpdates.updated_at = new Date().toISOString(); + + const { data: event, error } = await supabaseAdmin + .from("events") + .update(dbUpdates) + .eq("id", eventId) + .select() + .single(); + + if (error) { + console.error("Error updating event:", error); + return { success: false, error: error.message || "Failed to update event" }; + } + + // Revalidate pages that show events + revalidatePath("/dashboard/events"); + revalidatePath("/admin-dashboard"); + revalidatePath("/dashboard"); + + return { success: true, data: event }; + } catch (error: any) { + console.error("Error in updateEvent:", error); + return { success: false, error: "Internal server error" }; + } +} + +// DELETE an event (Admin only) +export async function deleteEvent(eventId: string): Promise { + try { + const session = await getServerSession(authOptions); + + if (!session?.user?.email) { + return { success: false, error: "Unauthorized" }; + } + + // Check if user is admin + const { data: userData } = await supabaseAdmin + .from("users") + .select("is_admin") + .eq("email", session.user.email) + .single(); + + if (userData?.is_admin !== 1) { + return { success: false, error: "Forbidden - Admin access required" }; + } + + const { error } = await supabaseAdmin + .from("events") + .delete() + .eq("id", eventId); + + if (error) { + console.error("Error deleting event:", error); + return { success: false, error: error.message || "Failed to delete event" }; + } + + // Revalidate pages that show events + revalidatePath("/dashboard/events"); + revalidatePath("/admin-dashboard"); + revalidatePath("/dashboard"); + + return { success: true, data: { id: eventId } }; + } catch (error: any) { + console.error("Error in deleteEvent:", error); + return { success: false, error: "Internal server error" }; + } +} + +// GET a specific event by ID +export async function getEventById(eventId: string): Promise { + try { + const session = await getServerSession(authOptions); + + if (!session?.user?.email) { + return { success: false, error: "Unauthorized" }; + } + + const { data: event, error } = await supabaseAdmin + .from("events") + .select("*") + .eq("id", eventId) + .single(); + + if (error) { + console.error("Error fetching event:", error); + return { success: false, error: "Event not found" }; + } + + return { success: true, data: event }; + } catch (error: any) { + console.error("Error in getEventById:", error); + return { success: false, error: "Internal server error" }; + } +} + +// GET featured events (Public - no auth required) +export async function getFeaturedEvents(): Promise> { + try { + const { data: events, error } = await supabaseAdmin + .from("events") + .select("*") + .eq("is_featured", true) + .order("date", { ascending: true }); + + if (error) { + console.error("Error fetching featured events:", error); + return { success: false, error: "Failed to fetch featured events" }; + } + + // Auto-update event statuses based on date + const updatedEvents = await autoUpdateEventStatuses(events); + + // Transform to camelCase for frontend + const transformedEvents = updatedEvents.map((event: any) => ({ + id: event.id, + title: event.title, + description: event.description, + date: event.date, + time: event.time, + location: event.location, + maxParticipants: event.maxparticipants, + registrationStatus: event.registrationstatus, + eventStatus: event.event_status, + category: event.category, + organizer: event.organizer, + imageUrl: event.imageurl, + tags: event.tags || [], + externalLink: event.external_link, + })); + + return { success: true, data: transformedEvents }; + } catch (error: any) { + console.error("Error in getFeaturedEvents:", error); + return { success: false, error: "Internal server error" }; + } +} diff --git a/app/actions/participants.ts b/app/actions/participants.ts new file mode 100644 index 0000000..e390133 --- /dev/null +++ b/app/actions/participants.ts @@ -0,0 +1,404 @@ +'use server' + +import { getServerSession } from "next-auth"; +import { authOptions } from "@/lib/auth"; +import { supabaseAdmin } from "@/lib/supabase-admin"; +import { revalidatePath } from "next/cache"; + +type ActionResult = + | { success: true; data: T } + | { success: false; error: string }; + +// GET all participants with optional filters +export async function getParticipants(filters?: { + eventId?: string; + userId?: string; +}): Promise> { + try { + const session = await getServerSession(authOptions); + + if (!session?.user?.email) { + return { success: false, error: "Unauthorized" }; + } + + let query = supabaseAdmin + .from("participants") + .select(` + *, + events:event_id ( + id, + title, + description, + time, + location, + category, + team_event + ), + users:user_id ( + uid, + name, + email, + picture, + phone, + year, + branch + ) + `) + .order("created_at", { ascending: false }); + + if (filters?.eventId) { + query = query.eq("event_id", filters.eventId); + } + + if (filters?.userId) { + query = query.eq("user_id", filters.userId); + } + + const { data: participants, error } = await query; + + if (error) { + console.error("Error fetching participants:", error); + return { success: false, error: "Failed to fetch participants" }; + } + + return { success: true, data: participants }; + } catch (error: any) { + console.error("Error in getParticipants:", error); + return { success: false, error: "Internal server error" }; + } +} + +// Register for an event (individual or team) +export async function registerForEvent(registration: { + event_id: string; + team_name?: string; + team_members?: Array<{ email: string; name?: string }>; +}): Promise { + try { + const session = await getServerSession(authOptions); + + if (!session?.user?.email) { + return { success: false, error: "Unauthorized" }; + } + + // Get user from database + const { data: user, error: userError } = await supabaseAdmin + .from("users") + .select("uid") + .eq("email", session.user.email) + .single(); + + if (userError || !user) { + return { success: false, error: "User not found" }; + } + + if (!registration.event_id) { + return { success: false, error: "Event ID is required" }; + } + + // Check if event exists and is open for registration + const { data: event, error: eventError } = await supabaseAdmin + .from("events") + .select("id, registrationstatus, maxparticipants, participantcount, team_event, max_team_size, min_team_size") + .eq("id", registration.event_id) + .single(); + + if (eventError || !event) { + return { success: false, error: "Event not found" }; + } + + // Check if registration is open (must be exactly 'open', not 'closed' or 'upcoming') + const registrationStatus = typeof event.registrationstatus === 'string' + ? event.registrationstatus.toLowerCase() + : ''; + + if (registrationStatus !== 'open') { + return { success: false, error: "Event registration is not open" }; + } + + if (event.maxparticipants && event.participantcount >= event.maxparticipants) { + return { success: false, error: "Event is full" }; + } + + // Check if user is already registered + const { data: existingRegistration } = await supabaseAdmin + .from("participants") + .select("id, team_name") + .eq("event_id", registration.event_id) + .eq("user_id", user.uid) + .single(); + + if (existingRegistration) { + return { + success: false, + error: existingRegistration.team_name + ? `You are already part of team "${existingRegistration.team_name}" for this event` + : "You are already registered for this event" + }; + } + + // Team event registration + if (event.team_event) { + if (!registration.team_name) { + return { success: false, error: "Team name is required for this event" }; + } + + if (!registration.team_members || !Array.isArray(registration.team_members)) { + return { success: false, error: "Team members are required for this event" }; + } + + // Validate team size + const totalTeamSize = registration.team_members.length + 1; + if (totalTeamSize < event.min_team_size || totalTeamSize > event.max_team_size) { + return { + success: false, + error: `Team size must be between ${event.min_team_size} and ${event.max_team_size} members` + }; + } + + const teamMemberEmails = registration.team_members.map(m => m.email); + + if (teamMemberEmails.includes(session.user.email)) { + return { success: false, error: "You don't need to add yourself to the team members list" }; + } + + // Verify all team members exist + const { data: teamMemberUsers, error: teamMemberError } = await supabaseAdmin + .from("users") + .select("uid, email") + .in("email", teamMemberEmails); + + if (teamMemberError || !teamMemberUsers || teamMemberUsers.length !== teamMemberEmails.length) { + return { success: false, error: "One or more team member emails are not registered users" }; + } + + // Check if any team member is already registered + const { data: existingTeamMembers } = await supabaseAdmin + .from("participants") + .select("user_id, team_name") + .eq("event_id", registration.event_id) + .in("user_id", teamMemberUsers.map(u => u.uid)); + + if (existingTeamMembers && existingTeamMembers.length > 0) { + const conflictingMember = teamMemberUsers.find( + u => existingTeamMembers.some(p => p.user_id === u.uid) + ); + const conflictingParticipant = existingTeamMembers.find( + p => p.user_id === conflictingMember?.uid + ); + return { + success: false, + error: `${conflictingMember?.email} is already registered${conflictingParticipant?.team_name ? ` in team "${conflictingParticipant.team_name}"` : ''} for this event` + }; + } + + // Get leader's info to include in team_members array + const { data: leaderInfo } = await supabaseAdmin + .from("users") + .select("email, name") + .eq("uid", user.uid) + .single(); + + // Create complete team_members array including the leader + const completeTeamMembers = [ + { email: leaderInfo?.email || session.user.email, name: leaderInfo?.name || session.user.name }, + ...registration.team_members + ]; + + // Register team leader + const { data: leaderParticipant, error: leaderError } = await supabaseAdmin + .from("participants") + .insert([ + { + event_id: registration.event_id, + user_id: user.uid, + status: "registered", + team_name: registration.team_name, + team_members: completeTeamMembers, + is_team_leader: true, + }, + ]) + .select() + .single(); + + if (leaderError) { + console.error("Error creating team leader:", leaderError); + return { success: false, error: "Failed to register team" }; + } + + // Register all team members with the complete team list + const teamMemberInserts = teamMemberUsers.map(member => ({ + event_id: registration.event_id, + user_id: member.uid, + status: "registered", + team_name: registration.team_name, + team_members: completeTeamMembers, + is_team_leader: false, + })); + + const { error: teamMembersError } = await supabaseAdmin + .from("participants") + .insert(teamMemberInserts); + + if (teamMembersError) { + console.error("Error creating team members:", teamMembersError); + // Rollback + await supabaseAdmin + .from("participants") + .delete() + .eq("id", leaderParticipant.id); + + return { success: false, error: "Failed to register team members" }; + } + + // Increment participant count + await supabaseAdmin + .from("events") + .update({ participantcount: event.participantcount + totalTeamSize }) + .eq("id", registration.event_id); + + revalidatePath("/dashboard/events"); + revalidatePath("/admin-dashboard"); + + return { success: true, data: leaderParticipant }; + } + + // Individual registration + const { data: participant, error: participantError } = await supabaseAdmin + .from("participants") + .insert([ + { + event_id: registration.event_id, + user_id: user.uid, + status: "registered", + team_name: null, + team_members: null, + is_team_leader: false, + }, + ]) + .select() + .single(); + + if (participantError) { + console.error("Error creating participant:", participantError); + return { success: false, error: "Failed to register for event" }; + } + + // Increment participant count + await supabaseAdmin + .from("events") + .update({ participantcount: event.participantcount + 1 }) + .eq("id", registration.event_id); + + revalidatePath("/dashboard/events"); + revalidatePath("/admin-dashboard"); + + return { success: true, data: participant }; + } catch (error: any) { + console.error("Error in registerForEvent:", error); + return { success: false, error: "Internal server error" }; + } +} + +// Update participant status (Admin only) +export async function updateParticipantStatus( + participantId: string, + updates: { status?: string; team_name?: string } +): Promise { + try { + const session = await getServerSession(authOptions); + + if (!session?.user?.email) { + return { success: false, error: "Unauthorized" }; + } + + // Check if user is admin + const { data: userData } = await supabaseAdmin + .from("users") + .select("is_admin") + .eq("email", session.user.email) + .single(); + + if (userData?.is_admin !== 1) { + return { success: false, error: "Forbidden - Admin access required" }; + } + + const { data: participant, error } = await supabaseAdmin + .from("participants") + .update(updates) + .eq("id", participantId) + .select() + .single(); + + if (error) { + console.error("Error updating participant:", error); + return { success: false, error: "Failed to update participant" }; + } + + revalidatePath("/admin-dashboard"); + + return { success: true, data: participant }; + } catch (error: any) { + console.error("Error in updateParticipantStatus:", error); + return { success: false, error: "Internal server error" }; + } +} + +// Delete participant (cancel registration) +export async function deleteParticipant(participantId: string): Promise { + try { + const session = await getServerSession(authOptions); + + if (!session?.user?.email) { + return { success: false, error: "Unauthorized" }; + } + + // Get participant info + const { data: participant } = await supabaseAdmin + .from("participants") + .select("event_id, user_id, users:user_id(email)") + .eq("id", participantId) + .single(); + + if (!participant) { + return { success: false, error: "Participant not found" }; + } + + // Check if user is admin or the participant themselves + const { data: userData } = await supabaseAdmin + .from("users") + .select("uid, is_admin") + .eq("email", session.user.email) + .single(); + + const isAdmin = userData?.is_admin === 1; + const isOwnRegistration = userData?.uid === participant.user_id; + + if (!isAdmin && !isOwnRegistration) { + return { success: false, error: "Forbidden - Cannot cancel another user's registration" }; + } + + const { error } = await supabaseAdmin + .from("participants") + .delete() + .eq("id", participantId); + + if (error) { + console.error("Error deleting participant:", error); + return { success: false, error: "Failed to cancel registration" }; + } + + // Decrement participant count + await supabaseAdmin.rpc('decrement_participant_count', { + event_id_param: participant.event_id + }); + + revalidatePath("/dashboard/events"); + revalidatePath("/admin-dashboard"); + + return { success: true, data: { id: participantId } }; + } catch (error: any) { + console.error("Error in deleteParticipant:", error); + return { success: false, error: "Internal server error" }; + } +} diff --git a/app/actions/projects.ts b/app/actions/projects.ts new file mode 100644 index 0000000..5727f7d --- /dev/null +++ b/app/actions/projects.ts @@ -0,0 +1,461 @@ +'use server' + +import { getServerSession } from "next-auth"; +import { authOptions } from "@/lib/auth"; +import { supabaseAdmin } from "@/lib/supabase-admin"; +import { revalidatePath } from "next/cache"; +import { Project, ProjectSubmission, ProjectReviewData, ProjectWithLikeStatus } from "@/types/projects"; + +type ActionResult = + | { success: true; data: T } + | { success: false; error: string }; + +// Convert database row to Project type +function dbRowToProject(row: any): Project { + return { + id: row.id, + title: row.title, + description: row.description, + fullDescription: row.full_description, + category: row.category, + tags: row.tags || [], + githubUrl: row.github_url, + liveUrl: row.live_url, + videoUrl: row.video_url, + imageUrl: row.image_url, + additionalImages: row.additional_images || [], + teamName: row.team_name, + teamMembers: row.team_members || [], + submittedBy: row.submitted_by, + submitterName: row.submitter_name, + submitterYear: row.submitter_year, + submitterBranch: row.submitter_branch, + status: row.status, + reviewNotes: row.review_notes, + reviewedBy: row.reviewed_by, + reviewedAt: row.reviewed_at, + isFeatured: row.is_featured, + viewsCount: row.views_count, + likesCount: row.likes_count, + createdAt: row.created_at, + updatedAt: row.updated_at, + }; +} + +// Get all approved projects for showcase +export async function getApprovedProjects( + category?: string, + featured?: boolean +): Promise> { + try { + let query = supabaseAdmin + .from('projects') + .select('*') + .eq('status', 'approved') + .order('created_at', { ascending: false }); + + if (category) { + query = query.eq('category', category); + } + + if (featured !== undefined) { + query = query.eq('is_featured', featured); + } + + const { data, error } = await query; + + if (error) throw error; + + return { + success: true, + data: data.map(dbRowToProject), + }; + } catch (error: any) { + console.error('Error fetching approved projects:', error); + return { success: false, error: error.message }; + } +} + +// Get single project by ID with view increment +export async function getProjectById(id: string, incrementView = false): Promise> { + try { + if (incrementView) { + // Increment view count + await supabaseAdmin.rpc('increment_project_views', { project_id: id }); + } + + const { data, error } = await supabaseAdmin + .from('projects') + .select('*') + .eq('id', id) + .single(); + + if (error) throw error; + + return { + success: true, + data: dbRowToProject(data), + }; + } catch (error: any) { + console.error('Error fetching project:', error); + return { success: false, error: error.message }; + } +} + +// Submit a new project +export async function submitProject(projectData: ProjectSubmission): Promise> { + try { + const session = await getServerSession(authOptions); + + if (!session?.user?.email) { + return { success: false, error: "You must be signed in to submit a project" }; + } + + // Get user profile for additional details + const { data: userProfile } = await supabaseAdmin + .from('users') + .select('name, year, branch') + .eq('email', session.user.email) + .single(); + + const { data, error } = await supabaseAdmin + .from('projects') + .insert({ + title: projectData.title, + description: projectData.description, + full_description: projectData.fullDescription, + category: projectData.category, + tags: projectData.tags, + github_url: projectData.githubUrl, + live_url: projectData.liveUrl, + video_url: projectData.videoUrl, + image_url: projectData.imageUrl, + additional_images: projectData.additionalImages, + team_name: projectData.teamName, + team_members: projectData.teamMembers, + submitted_by: session.user.email, + submitter_name: userProfile?.name || session.user.name || 'Unknown', + submitter_year: userProfile?.year, + submitter_branch: userProfile?.branch, + status: 'pending', + }) + .select() + .single(); + + if (error) throw error; + + revalidatePath('/projects'); + revalidatePath('/admin-dashboard'); + + return { + success: true, + data: dbRowToProject(data), + }; + } catch (error: any) { + console.error('Error submitting project:', error); + return { success: false, error: error.message }; + } +} + +// Get user's own projects +export async function getMyProjects(): Promise> { + try { + const session = await getServerSession(authOptions); + + if (!session?.user?.email) { + return { success: false, error: "You must be signed in" }; + } + + const { data, error } = await supabaseAdmin + .from('projects') + .select('*') + .eq('submitted_by', session.user.email) + .order('created_at', { ascending: false }); + + if (error) throw error; + + return { + success: true, + data: data.map(dbRowToProject), + }; + } catch (error: any) { + console.error('Error fetching user projects:', error); + return { success: false, error: error.message }; + } +} + +// Admin: Get all projects (including pending) +export async function getAllProjects(status?: string): Promise> { + try { + const session = await getServerSession(authOptions); + + if (!session?.user?.email) { + return { success: false, error: "You must be signed in" }; + } + + // Check if user is admin + const { data: userProfile } = await supabaseAdmin + .from('users') + .select('is_admin') + .eq('email', session.user.email) + .single(); + + if (!userProfile?.is_admin) { + return { success: false, error: "Unauthorized: Admin access required" }; + } + + let query = supabaseAdmin + .from('projects') + .select('*') + .order('created_at', { ascending: false }); + + if (status) { + query = query.eq('status', status); + } + + const { data, error } = await query; + + if (error) throw error; + + return { + success: true, + data: data.map(dbRowToProject), + }; + } catch (error: any) { + console.error('Error fetching all projects:', error); + return { success: false, error: error.message }; + } +} + +// Admin: Review a project +export async function reviewProject( + projectId: string, + reviewData: ProjectReviewData +): Promise> { + try { + const session = await getServerSession(authOptions); + + if (!session?.user?.email) { + return { success: false, error: "You must be signed in" }; + } + + // Check if user is admin + const { data: userProfile } = await supabaseAdmin + .from('users') + .select('is_admin') + .eq('email', session.user.email) + .single(); + + if (!userProfile?.is_admin) { + return { success: false, error: "Unauthorized: Admin access required" }; + } + + const { data, error } = await supabaseAdmin + .from('projects') + .update({ + status: reviewData.status, + review_notes: reviewData.reviewNotes, + reviewed_by: session.user.email, + reviewed_at: new Date().toISOString(), + }) + .eq('id', projectId) + .select() + .single(); + + if (error) throw error; + + revalidatePath('/projects'); + revalidatePath('/admin-dashboard'); + + return { + success: true, + data: dbRowToProject(data), + }; + } catch (error: any) { + console.error('Error reviewing project:', error); + return { success: false, error: error.message }; + } +} + +// Admin: Toggle featured status +export async function toggleFeaturedProject(projectId: string): Promise> { + try { + const session = await getServerSession(authOptions); + + if (!session?.user?.email) { + return { success: false, error: "You must be signed in" }; + } + + // Check if user is admin + const { data: userProfile } = await supabaseAdmin + .from('users') + .select('is_admin') + .eq('email', session.user.email) + .single(); + + if (!userProfile?.is_admin) { + return { success: false, error: "Unauthorized: Admin access required" }; + } + + // Get current featured status + const { data: project } = await supabaseAdmin + .from('projects') + .select('is_featured') + .eq('id', projectId) + .single(); + + const { data, error } = await supabaseAdmin + .from('projects') + .update({ + is_featured: !project?.is_featured, + }) + .eq('id', projectId) + .select() + .single(); + + if (error) throw error; + + revalidatePath('/projects'); + revalidatePath('/admin-dashboard'); + + return { + success: true, + data: dbRowToProject(data), + }; + } catch (error: any) { + console.error('Error toggling featured status:', error); + return { success: false, error: error.message }; + } +} + +// Like/Unlike a project +export async function toggleProjectLike(projectId: string): Promise> { + try { + const session = await getServerSession(authOptions); + + if (!session?.user?.email) { + return { success: false, error: "You must be signed in to like projects" }; + } + + // Check if already liked + const { data: existingLike } = await supabaseAdmin + .from('project_likes') + .select('id') + .eq('project_id', projectId) + .eq('user_email', session.user.email) + .single(); + + if (existingLike) { + // Unlike + await supabaseAdmin + .from('project_likes') + .delete() + .eq('id', existingLike.id); + } else { + // Like + await supabaseAdmin + .from('project_likes') + .insert({ + project_id: projectId, + user_email: session.user.email, + }); + } + + // Get updated like count + const { count } = await supabaseAdmin + .from('project_likes') + .select('*', { count: 'exact', head: true }) + .eq('project_id', projectId); + + // Update project likes count + await supabaseAdmin + .from('projects') + .update({ likes_count: count || 0 }) + .eq('id', projectId); + + revalidatePath('/projects'); + + return { + success: true, + data: { + liked: !existingLike, + likesCount: count || 0, + }, + }; + } catch (error: any) { + console.error('Error toggling project like:', error); + return { success: false, error: error.message }; + } +} + +// Check if user has liked a project +export async function hasUserLikedProject(projectId: string): Promise> { + try { + const session = await getServerSession(authOptions); + + if (!session?.user?.email) { + return { success: true, data: false }; + } + + const { data } = await supabaseAdmin + .from('project_likes') + .select('id') + .eq('project_id', projectId) + .eq('user_email', session.user.email) + .single(); + + return { + success: true, + data: !!data, + }; + } catch (error: any) { + return { success: true, data: false }; + } +} + +// Delete project (admin or owner) +export async function deleteProject(projectId: string): Promise> { + try { + const session = await getServerSession(authOptions); + + if (!session?.user?.email) { + return { success: false, error: "You must be signed in" }; + } + + // Get project to check ownership + const { data: project } = await supabaseAdmin + .from('projects') + .select('submitted_by') + .eq('id', projectId) + .single(); + + // Check if user is admin + const { data: userProfile } = await supabaseAdmin + .from('users') + .select('is_admin') + .eq('email', session.user.email) + .single(); + + const isOwner = project?.submitted_by === session.user.email; + const isAdmin = userProfile?.is_admin; + + if (!isOwner && !isAdmin) { + return { success: false, error: "Unauthorized: You can only delete your own projects" }; + } + + const { error } = await supabaseAdmin + .from('projects') + .delete() + .eq('id', projectId); + + if (error) throw error; + + revalidatePath('/projects'); + revalidatePath('/admin-dashboard'); + + return { success: true, data: undefined }; + } catch (error: any) { + console.error('Error deleting project:', error); + return { success: false, error: error.message }; + } +} diff --git a/app/actions/teams.ts b/app/actions/teams.ts new file mode 100644 index 0000000..7e5837e --- /dev/null +++ b/app/actions/teams.ts @@ -0,0 +1,355 @@ +'use server' + +import { getServerSession } from "next-auth"; +import { authOptions } from "@/lib/auth"; +import { supabaseAdmin } from "@/lib/supabase-admin"; +import { revalidatePath } from "next/cache"; + +type ActionResult = + | { success: true; data: T } + | { success: false; error: string }; + +// GET user's team for an event +export async function getMyTeam(eventId: string): Promise { + try { + const session = await getServerSession(authOptions); + + if (!session?.user?.email) { + return { success: false, error: "Unauthorized" }; + } + + // Get user + const { data: user } = await supabaseAdmin + .from("users") + .select("uid") + .eq("email", session.user.email) + .single(); + + if (!user) { + return { success: false, error: "User not found" }; + } + + // Get participant with team info + const { data: participant, error } = await supabaseAdmin + .from("participants") + .select(` + *, + users:user_id ( + name, + email, + picture + ) + `) + .eq("event_id", eventId) + .eq("user_id", user.uid) + .single(); + + if (error || !participant) { + return { success: false, error: "Not registered for this event" }; + } + + return { success: true, data: participant }; + } catch (error: any) { + console.error("Error in getMyTeam:", error); + return { success: false, error: "Internal server error" }; + } +} + +// ADD a team member (Team leader only) +export async function addTeamMember( + eventId: string, + memberEmail: string +): Promise { + try { + const session = await getServerSession(authOptions); + + if (!session?.user?.email) { + return { success: false, error: "Unauthorized" }; + } + + if (!eventId || !memberEmail) { + return { success: false, error: "Event ID and member email are required" }; + } + + // Get current user + const { data: user } = await supabaseAdmin + .from("users") + .select("uid, email") + .eq("email", session.user.email) + .single(); + + if (!user) { + return { success: false, error: "User not found" }; + } + + // Check if user is team leader + const { data: leaderParticipant, error: leaderError } = await supabaseAdmin + .from("participants") + .select("id, team_name, team_members, is_team_leader") + .eq("event_id", eventId) + .eq("user_id", user.uid) + .single(); + + if (leaderError || !leaderParticipant) { + return { success: false, error: "Not registered for this event" }; + } + + if (!leaderParticipant.is_team_leader) { + return { success: false, error: "Only team leaders can add members" }; + } + + // Get event info for team size limits + const { data: event } = await supabaseAdmin + .from("events") + .select("max_team_size, min_team_size, participantcount, maxparticipants") + .eq("id", eventId) + .single(); + + if (!event) { + return { success: false, error: "Event not found" }; + } + + // Check team size limit (team_members now includes leader) + const currentTeamSize = leaderParticipant.team_members?.length || 0; + if (currentTeamSize >= event.max_team_size) { + return { + success: false, + error: `Team is already at maximum size (${event.max_team_size})` + }; + } + + // Check if event is full + if (event.participantcount >= event.maxparticipants) { + return { success: false, error: "Event is full" }; + } + + // Get new member user + const { data: newMember } = await supabaseAdmin + .from("users") + .select("uid, email, name") + .eq("email", memberEmail) + .single(); + + if (!newMember) { + return { success: false, error: "Member not found. User must be registered." }; + } + + // Check if member is already registered + const { data: existingParticipant } = await supabaseAdmin + .from("participants") + .select("id, team_name") + .eq("event_id", eventId) + .eq("user_id", newMember.uid) + .single(); + + if (existingParticipant) { + return { + success: false, + error: `${memberEmail} is already registered${existingParticipant.team_name ? ` in team "${existingParticipant.team_name}"` : ''} for this event` + }; + } + + // Update team members array for all team participants + const updatedTeamMembers = [ + ...(leaderParticipant.team_members || []), + { email: newMember.email, name: newMember.name }, + ]; + + // Update all existing team members + const { error: updateError } = await supabaseAdmin + .from("participants") + .update({ team_members: updatedTeamMembers }) + .eq("event_id", eventId) + .eq("team_name", leaderParticipant.team_name); + + if (updateError) { + console.error("Error updating team members:", updateError); + return { success: false, error: "Failed to update team" }; + } + + // Register new member + const { error: insertError } = await supabaseAdmin + .from("participants") + .insert([{ + event_id: eventId, + user_id: newMember.uid, + status: "registered", + team_name: leaderParticipant.team_name, + team_members: updatedTeamMembers, + is_team_leader: false, + }]); + + if (insertError) { + console.error("Error registering new member:", insertError); + return { success: false, error: "Failed to add team member" }; + } + + // Increment participant count + await supabaseAdmin + .from("events") + .update({ participantcount: event.participantcount + 1 }) + .eq("id", eventId); + + revalidatePath("/dashboard/events"); + + return { + success: true, + data: { + message: "Team member added successfully", + team_members: updatedTeamMembers + } + }; + } catch (error: any) { + console.error("Error in addTeamMember:", error); + return { success: false, error: "Internal server error" }; + } +} + +// REMOVE a team member (Team leader only) +export async function removeTeamMember( + eventId: string, + memberEmail: string +): Promise { + try { + const session = await getServerSession(authOptions); + + if (!session?.user?.email) { + return { success: false, error: "Unauthorized" }; + } + + if (!eventId || !memberEmail) { + return { success: false, error: "Event ID and member email are required" }; + } + + // Get current user + const { data: user } = await supabaseAdmin + .from("users") + .select("uid, email") + .eq("email", session.user.email) + .single(); + + if (!user) { + return { success: false, error: "User not found" }; + } + + // Check if user is team leader + const { data: leaderParticipant, error: leaderError } = await supabaseAdmin + .from("participants") + .select("id, team_name, team_members, is_team_leader") + .eq("event_id", eventId) + .eq("user_id", user.uid) + .single(); + + if (leaderError || !leaderParticipant) { + return { success: false, error: "Not registered for this event" }; + } + + if (!leaderParticipant.is_team_leader) { + return { success: false, error: "Only team leaders can remove members" }; + } + + // Get event info for team size limits + const { data: event } = await supabaseAdmin + .from("events") + .select("min_team_size, participantcount") + .eq("id", eventId) + .single(); + + if (!event) { + return { success: false, error: "Event not found" }; + } + + // Get member to remove + const { data: memberToRemove } = await supabaseAdmin + .from("users") + .select("uid, email") + .eq("email", memberEmail) + .single(); + + if (!memberToRemove) { + return { success: false, error: "Member not found" }; + } + + // Update team members array (now includes leader, so just filter) + const updatedTeamMembers = (leaderParticipant.team_members || []).filter( + (m: any) => m.email !== memberEmail + ); + + const currentTeamSize = leaderParticipant.team_members?.length || 0; + const newTeamSize = updatedTeamMembers.length; + const wouldBeUnderMinimum = newTeamSize < event.min_team_size; + + if (wouldBeUnderMinimum) { + // Unregister entire team + const { error: deleteTeamError } = await supabaseAdmin + .from("participants") + .delete() + .eq("event_id", eventId) + .eq("team_name", leaderParticipant.team_name); + + if (deleteTeamError) { + console.error("Error unregistering team:", deleteTeamError); + return { success: false, error: "Failed to unregister team" }; + } + + // Decrement participant count by the number of team members + await supabaseAdmin + .from("events") + .update({ participantcount: Math.max(0, event.participantcount - currentTeamSize) }) + .eq("id", eventId); + + revalidatePath("/dashboard/events"); + + return { + success: true, + data: { + message: "Team unregistered (below minimum size)", + unregistered: true + } + }; + } + + // Update all remaining team members + const { error: updateError } = await supabaseAdmin + .from("participants") + .update({ team_members: updatedTeamMembers }) + .eq("event_id", eventId) + .eq("team_name", leaderParticipant.team_name); + + if (updateError) { + console.error("Error updating team members:", updateError); + return { success: false, error: "Failed to update team" }; + } + + // Remove member's participant record + const { error: deleteError } = await supabaseAdmin + .from("participants") + .delete() + .eq("event_id", eventId) + .eq("user_id", memberToRemove.uid); + + if (deleteError) { + console.error("Error removing member:", deleteError); + return { success: false, error: "Failed to remove team member" }; + } + + // Decrement participant count + await supabaseAdmin + .from("events") + .update({ participantcount: Math.max(0, event.participantcount - 1) }) + .eq("id", eventId); + + revalidatePath("/dashboard/events"); + + return { + success: true, + data: { + message: "Team member removed successfully", + team_members: updatedTeamMembers + } + }; + } catch (error: any) { + console.error("Error in removeTeamMember:", error); + return { success: false, error: "Internal server error" }; + } +} diff --git a/app/actions/users.ts b/app/actions/users.ts new file mode 100644 index 0000000..9c216f7 --- /dev/null +++ b/app/actions/users.ts @@ -0,0 +1,109 @@ +'use server' + +import { getServerSession } from "next-auth"; +import { authOptions } from "@/lib/auth"; +import { supabaseAdmin } from "@/lib/supabase-admin"; + +type ActionResult = + | { success: true; data: T } + | { success: false; error: string }; + +// Search for a user by email +export async function searchUserByEmail(email: string): Promise> { + try { + const session = await getServerSession(authOptions); + + if (!session?.user?.email) { + return { success: false, error: "Unauthorized" }; + } + + if (!email) { + return { success: false, error: "Email is required" }; + } + + // Search for user by email + const { data: user, error } = await supabaseAdmin + .from("users") + .select("email, name, picture") + .eq("email", email) + .single(); + + if (error) { + if (error.code === "PGRST116") { + // No rows returned + return { success: true, data: { exists: false, user: null } }; + } + console.error("Error searching user:", error); + return { success: false, error: "Failed to search user" }; + } + + return { success: true, data: { exists: true, user } }; + } catch (error: any) { + console.error("Error in searchUserByEmail:", error); + return { success: false, error: "Internal server error" }; + } +} + +// Get current user profile +export async function getCurrentUserProfile(): Promise { + try { + const session = await getServerSession(authOptions); + + if (!session?.user?.email) { + return { success: false, error: "Unauthorized" }; + } + + const { data: user, error } = await supabaseAdmin + .from("users") + .select("uid, name, email, picture, phone, year, branch, created_at, is_admin, graduated") + .eq("email", session.user.email) + .single(); + + if (error) { + console.error("Error fetching user profile:", error); + return { success: false, error: "User not found" }; + } + + return { success: true, data: user }; + } catch (error: any) { + console.error("Error in getCurrentUserProfile:", error); + return { success: false, error: "Internal server error" }; + } +} + +// Get all users (Admin only) +export async function getAllUsers(): Promise> { + try { + const session = await getServerSession(authOptions); + + if (!session?.user?.email) { + return { success: false, error: "Unauthorized" }; + } + + // Check if user is admin + const { data: userData } = await supabaseAdmin + .from("users") + .select("is_admin") + .eq("email", session.user.email) + .single(); + + if (userData?.is_admin !== 1) { + return { success: false, error: "Forbidden - Admin access required" }; + } + + const { data: users, error } = await supabaseAdmin + .from("users") + .select("*") + .order("created_at", { ascending: false }); + + if (error) { + console.error("Error fetching users:", error); + return { success: false, error: "Failed to fetch users" }; + } + + return { success: true, data: users }; + } catch (error: any) { + console.error("Error in getAllUsers:", error); + return { success: false, error: "Internal server error" }; + } +} diff --git a/app/admin-dashboard/page.tsx b/app/admin-dashboard/page.tsx new file mode 100644 index 0000000..cb59b2e --- /dev/null +++ b/app/admin-dashboard/page.tsx @@ -0,0 +1,299 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { useState, useEffect } from "react"; +import { motion } from "framer-motion"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Calendar, Users, Settings, BarChart3, Folder } from "lucide-react"; +import EventManagement from "@/components/admin/EventManagement"; +import ParticipantManagement from "@/components/admin/ParticipantManagement"; +import AdminStats from "@/components/admin/AdminStats"; +import ProjectManagement from "@/components/admin/ProjectManagement"; +import { Event, EventWithStats } from "@/types/events"; +import { Project } from "@/types/projects"; +import { getEvents, createEvent, updateEvent, deleteEvent } from "@/app/actions/events"; +import { getAllProjects } from "@/app/actions/projects"; +import { getCurrentUserProfile } from "@/app/actions/users"; + +interface UserProfile { + name: string; + email: string; + picture: string; + branch?: string; + year?: number; + is_admin: number; +} + +export default function AdminDashboard() { + const router = useRouter(); + const [events, setEvents] = useState([]); + const [projects, setProjects] = useState([]); + const [activeTab, setActiveTab] = useState("overview"); + const [loading, setLoading] = useState(true); + const [userProfile, setUserProfile] = useState(null); + + // Fetch events and user profile from API + useEffect(() => { + fetchEventsData(); + fetchProjectsData(); + fetchUserProfile(); + }, []); + + const fetchUserProfile = async () => { + try { + const result = await getCurrentUserProfile(); + + if (result.success && result.data) { + setUserProfile(result.data as UserProfile); + } + } catch (error) { + console.error('Error fetching user profile:', error); + } + }; + + const fetchEventsData = async () => { + try { + setLoading(true); + const result = await getEvents(); + + if (result.success) { + // Transform the data to match EventWithStats interface + const transformedEvents = result.data.map((event: any) => ({ + id: event.id, + title: event.title, + description: event.description, + date: event.date || event.created_at?.split('T')[0] || new Date().toISOString().split('T')[0], + time: event.time, + location: event.location, + maxParticipants: event.maxparticipants, + registrationstatus: event.registrationstatus || 'open', + registrationDeadline: event.registration_deadline, + category: event.category, + organizer: event.organizer, + imageUrl: event.imageurl, + tags: event.tags || [], + requirements: event.requirements || [], + createdAt: event.created_at, + updatedAt: event.updated_at, + participantCount: event.participantcount || 0, + teamEvent: event.team_event || false, + maxTeamSize: event.max_team_size || 1, + minTeamSize: event.min_team_size || 1, + eventStatus: event.event_status, + isFeatured: event.is_featured, + stats: event.stats || { + total: 0, + confirmed: 0, + attended: 0, + cancelled: 0 + } + })); + setEvents(transformedEvents); + } + } catch (error) { + console.error('Error fetching events:', error); + } finally { + setLoading(false); + } + }; + + const fetchProjectsData = async () => { + try { + const result = await getAllProjects(); + if (result.success) { + setProjects(result.data); + } + } catch (error) { + console.error('Error fetching projects:', error); + } + }; + + const handleCreateEvent = async (eventData: Event) => { + try { + const result = await createEvent({ + title: eventData.title, + description: eventData.description, + date: eventData.date, + time: eventData.time, + location: eventData.location, + maxParticipants: eventData.maxParticipants, + registrationstatus: eventData.registrationstatus || 'open', + registrationDeadline: eventData.registrationDeadline, + category: eventData.category, + organizer: eventData.organizer, + imageUrl: eventData.imageUrl, + tags: eventData.tags, + requirements: eventData.requirements, + teamEvent: eventData.teamEvent || false, + maxTeamSize: eventData.maxTeamSize || 1, + minTeamSize: eventData.minTeamSize || 1, + }); + + if (!result.success) { + throw new Error(result.error || 'Failed to create event'); + } + + // Refresh events list + await fetchEventsData(); + } catch (error) { + console.error('Error creating event:', error); + alert('Failed to create event. Please try again.'); + } + }; + + const handleUpdateEvent = async (eventId: string, updates: Partial) => { + try { + const result = await updateEvent(eventId, updates); + + if (!result.success) { + throw new Error(result.error || 'Failed to update event'); + } + + // Refresh events list + await fetchEventsData(); + } catch (error) { + console.error('Error updating event:', error); + alert('Failed to update event. Please try again.'); + } + }; + + const handleDeleteEvent = async (eventId: string) => { + if (!confirm('Are you sure you want to delete this event?')) { + return; + } + + try { + const result = await deleteEvent(eventId); + + if (!result.success) { + throw new Error(result.error || 'Failed to delete event'); + } + + // Refresh events list + await fetchEventsData(); + } catch (error) { + console.error('Error deleting event:', error); + alert('Failed to delete event. Please try again.'); + } + }; + + return ( +
+ {/* Header */} +
+
+
+
+

+ Admin Dashboard +

+

+ Manage events and participants +

+
+
+ {userProfile ? ( +
+ {userProfile.name} +
+

+ {userProfile.name} +

+

+ Admin {userProfile.branch ? `• ${userProfile.branch}` : ''} +

+
+
+ ) : ( + + Welcome, Admin + + )} + +
+
+
+
+ + {/* Main Content */} +
+ + + + + Overview + + + + Events + + + + Participants + + + + Projects + + + + + {loading ? ( +
+
+ {[1, 2, 3].map((i) => ( +
+ ))} +
+
+
+ ) : ( + <> + + + + + + + + + + + + + + + + + )} + + +
+
+ ); +} diff --git a/app/ai-group/page.tsx b/app/ai-group/page.tsx index aa16191..4ad5c53 100644 --- a/app/ai-group/page.tsx +++ b/app/ai-group/page.tsx @@ -1,39 +1,31 @@ import Navbar from "@/components/Navbar"; -import { SectionHero } from "@/components/sections/SectionHero"; -import { ResearchProjects } from "@/components/ai-group/ResearchProjects"; +import { HeroSection } from "@/components/sections/HeroSection"; import { TeamMembers } from "@/components/ai-group/TeamMembers"; -import { ResourcesHub } from "@/components/ai-group/ResourcesHub"; import { Events } from "@/components/ai-group/Events"; -import { ProjectGallery } from "@/components/ai-group/ProjectGallery"; -import { JoinCTA } from "@/components/ai-group/JoinCTA"; import { Suspense } from "react"; export default function AIGroupPage() { const badges = [ - { label: "Machine Learning", color: "bg-green-500/10 text-green-300" }, - { label: "Deep Learning", color: "bg-emerald-500/10 text-emerald-300" }, - { label: "Computer Vision", color: "bg-green-500/10 text-green-300" }, - { label: "NLP", color: "bg-emerald-500/10 text-emerald-300" }, - { label: "Neural Networks", color: "bg-green-500/10 text-green-300" }, - { label: "Data Science", color: "bg-emerald-500/10 text-emerald-300" }, + { label: "Machine Learning", className: "bg-green-500/10 text-green-300" }, + { label: "Deep Learning", className: "bg-emerald-500/10 text-emerald-300" }, + { label: "Computer Vision", className: "bg-green-500/10 text-green-300" }, + { label: "NLP", className: "bg-emerald-500/10 text-emerald-300" }, + { label: "Neural Networks", className: "bg-green-500/10 text-green-300" }, + { label: "Data Science", className: "bg-emerald-500/10 text-emerald-300" }, ]; return ( Loading...
}>
- - - - -
); diff --git a/app/api/auth/[...nextauth]/route.ts b/app/api/auth/[...nextauth]/route.ts index 770b631..07349df 100644 --- a/app/api/auth/[...nextauth]/route.ts +++ b/app/api/auth/[...nextauth]/route.ts @@ -1,40 +1,10 @@ -import { type NextRequest } from 'next/server'; -import { Auth, type AuthConfig } from '@auth/core'; -import Google from '@auth/core/providers/google'; - -export const runtime = 'edge'; - -const config: AuthConfig = { - providers: [ - Google({ - clientId: process.env.GOOGLE_CLIENT_ID!, - clientSecret: process.env.GOOGLE_CLIENT_SECRET!, - }), - ], - callbacks: { - async signIn({ account, profile }) { - if (account?.provider === "google" && profile?.email) { - return profile.email.endsWith('.vjti.ac.in'); - } - return false; - }, - async redirect({ url, baseUrl }) { - if (url.startsWith(baseUrl)) return url; - else if (url.startsWith("/")) return new URL(url, baseUrl).toString(); - return baseUrl + "/dashboard"; - }, - }, - pages: { - signIn: "/auth/signin", - error: "/auth/error", - }, - secret: process.env.NEXTAUTH_SECRET, -}; - -export async function GET(request: NextRequest) { - return await Auth(request, config); -} - -export async function POST(request: NextRequest) { - return await Auth(request, config); -} \ No newline at end of file +// In app/api/auth/[...nextauth]/route.ts +import NextAuth from "next-auth" +import { authOptions } from "@/lib/auth" + +// Use the shared `authOptions` from `lib/auth.ts`. Next.js Route files +// should only export valid Route handlers (GET, POST, etc.). Exporting +// arbitrary values like `authOptions` from a Route file causes type errors +// during build. +const handler = NextAuth(authOptions as any) +export { handler as GET, handler as POST } \ No newline at end of file diff --git a/app/api/resources/[domain]/route.ts b/app/api/resources/[domain]/route.ts index dee1a55..1dd0c28 100644 --- a/app/api/resources/[domain]/route.ts +++ b/app/api/resources/[domain]/route.ts @@ -1,15 +1,9 @@ import { NextResponse } from 'next/server'; import { NextRequest } from 'next/server'; +import resources from '@/data/resources.json'; export const runtime = 'edge'; -const resources = { - "cp": [], - "dev": [], - "eth": [], - "ai": [], - "proj-x": [] -}; export async function GET( request: NextRequest, @@ -18,7 +12,7 @@ export async function GET( try { const domain = params.domain; const domainResources = resources[domain as keyof typeof resources] || []; - + return NextResponse.json(domainResources); } catch { return NextResponse.json( diff --git a/app/cp-club/page.tsx b/app/cp-club/page.tsx index 6a90020..8311779 100644 --- a/app/cp-club/page.tsx +++ b/app/cp-club/page.tsx @@ -1,29 +1,25 @@ import Navbar from "@/components/Navbar"; -import { SectionHero } from "@/components/sections/SectionHero"; +import { AboutSection } from "../../components/cp-club/sections/about-section"; +import AchievementsSection from "../../components/cp-club/sections/acheivements-section"; +import { Events } from "../../components/cp-club/sections/events-section"; +import { TeamSection } from "../../components/cp-club/sections/teams-section"; import { Suspense } from "react"; +import HeroSection from "../../components/cp-club/sections/hero-section"; export default function CpClubPage() { - const badges = [ - { label: "Web Development", color: "bg-green-500/10 text-green-300" }, - { label: "Mobile Apps", color: "bg-emerald-500/10 text-emerald-300" }, - { label: "Cloud Computing", color: "bg-green-500/10 text-green-300" }, - { label: "DevOps", color: "bg-emerald-500/10 text-emerald-300" }, - { label: "System Design", color: "bg-green-500/10 text-green-300" }, - { label: "Full Stack", color: "bg-emerald-500/10 text-emerald-300" }, - ]; return ( Loading...
}> +
- - - {/* Other sections will go here */} + + + + + {/* */} + +
); -} \ No newline at end of file +} \ No newline at end of file diff --git a/app/dashboard/[domain]/page.tsx b/app/dashboard/[domain]/page.tsx index 51baff0..5d5c989 100644 --- a/app/dashboard/[domain]/page.tsx +++ b/app/dashboard/[domain]/page.tsx @@ -1,90 +1,152 @@ "use client"; - -// import { useSession } from "next-auth/react"; +import React from "react"; +import { useSession } from "next-auth/react"; import { useParams } from "next/navigation"; import { domains } from "@/config/navigation"; import { motion } from "framer-motion"; import { Card } from "@/components/ui/card"; import { ScrollArea } from "@/components/ui/scroll-area"; +import { Info, X } from "lucide-react"; import ResourceTable from "@/components/ResourceTable"; interface Domain { - name: string; - resources: string; - icon: React.ComponentType; - gradient: string; - description: string; - categories: string[]; - } - -export const runtime = 'edge'; + name: string; + resources: string; + icon: React.ComponentType; + gradient: string; + description: string; + categories: string[]; +} export default function DomainPage() { -// const { data: session } = useSession(); + const { data: session } = useSession(); const params = useParams(); + const [isModalOpen, setIsModalOpen] = React.useState(false); + const currentDomain = domains.find( (d: Domain) => d.resources === params.domain ); - - if (!currentDomain) return null; + if (!currentDomain) return
{params.domain}
; + + const [activeTab, setActiveTab] = React.useState(currentDomain.categories[0]); + + React.useEffect(() => { + const metaViewport = document.querySelector('meta[name="viewport"]'); + if (metaViewport) { + metaViewport.setAttribute('content', 'width=device-width, initial-scale=1.0, maximum-scale=5.0, user-scalable=yes'); + } + }, []); return ( -
+
+ + + {isModalOpen && ( +
setIsModalOpen(false)} + > + e.stopPropagation()} + > +
+

+ About {currentDomain.name} +

+ +
+

+ {currentDomain.description} +

+
+
+ )} + + - {/* Welcome Section */} -
-

+
+

{currentDomain.name}

-

+

Browse through {currentDomain.name.toLowerCase()} resources curated for VJTI students.

- - {/* Resources Section */} + -
-
-
- +
+
+
+
-
-

+
+

Resources

-

+

{currentDomain.description}

+

- -
+
+
{currentDomain.categories.map((category: string) => ( -
-
-
-

- {category} -

-
- - - -
+ ))}
- + + + + + + + + +
); -} \ No newline at end of file +} diff --git a/app/dashboard/events/page.tsx b/app/dashboard/events/page.tsx new file mode 100644 index 0000000..a170c64 --- /dev/null +++ b/app/dashboard/events/page.tsx @@ -0,0 +1,469 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { useSession } from "next-auth/react"; +import { motion } from "framer-motion"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Skeleton } from "@/components/ui/skeleton"; +import { + Calendar, + MapPin, + Users, + Clock, + Tag, + CheckCircle, + XCircle, + AlertCircle, + UsersRound, + Info +} from "lucide-react"; +import { format, parseISO } from "date-fns"; +import { TeamRegistrationModal } from "@/components/TeamRegistrationModal"; +import { EventDetailsModal } from "@/components/EventDetailsModal"; +import { getEvents } from "@/app/actions/events"; +import { getParticipants, registerForEvent } from "@/app/actions/participants"; +import { getCurrentUserProfile } from "@/app/actions/users"; + +interface Event { + id: string; + title: string; + description: string; + time: string; + date?: string; + location: string; + maxparticipants: number; + registrationstatus: string | boolean; + registration_deadline?: string; + event_status?: string; + category: string; + organizer: string; + tags: string[]; + requirements: string[]; + participantcount: number; + created_at: string; + team_event?: boolean; + max_team_size?: number; + min_team_size?: number; + imageurl?: string; +} + +export default function EventsPage() { + const { data: session } = useSession(); + const [events, setEvents] = useState([]); + const [loading, setLoading] = useState(true); + const [filter, setFilter] = useState("all"); + const [registering, setRegistering] = useState(null); + const [teamModalOpen, setTeamModalOpen] = useState(false); + const [detailsModalOpen, setDetailsModalOpen] = useState(false); + const [selectedEvent, setSelectedEvent] = useState(null); + const [registeredEventIds, setRegisteredEventIds] = useState>(new Set()); + + useEffect(() => { + fetchEvents(); + fetchUserRegistrations(); + }, []); + + const fetchEvents = async () => { + try { + setLoading(true); + const result = await getEvents(); + + if (result.success && result.data) { + setEvents(result.data); + } else if (!result.success) { + console.error("Error fetching events:", result.error); + } + } catch (error) { + console.error("Error fetching events:", error); + } finally { + setLoading(false); + } + }; + + const fetchUserRegistrations = async () => { + try { + // Get current user's UID + const userResult = await getCurrentUserProfile(); + if (!userResult.success || !userResult.data) { + console.error("Failed to get user profile:", !userResult.success ? (userResult as any).error : "No user data"); + return; + } + + const userId = userResult.data.uid; + + // Fetch only this user's registrations + const result = await getParticipants({ userId }); + + if (result.success && result.data) { + const eventIds = new Set( + result.data.map((p: any) => p.event_id as string) + ); + setRegisteredEventIds(eventIds); + } + } catch (error) { + console.error("Error fetching registrations:", error); + } + }; + + const handleRegister = async (event: Event) => { + // If it's a team event, open the team registration modal + if (event.team_event) { + setSelectedEvent(event); + setTeamModalOpen(true); + return; + } + + // Individual registration + try { + setRegistering(event.id); + const result = await registerForEvent({ event_id: event.id }); + + if (result.success) { + alert("Successfully registered for the event!"); + await fetchEvents(); + await fetchUserRegistrations(); + } else { + alert(result.error || "Failed to register"); + } + } catch (error: any) { + console.error("Error registering:", error); + alert(error.message || "Failed to register. Please try again."); + } finally { + setRegistering(null); + } + }; + + const handleTeamRegistrationSuccess = async () => { + await fetchEvents(); + await fetchUserRegistrations(); + setTeamModalOpen(false); + }; + + const handleTeamUpdate = async () => { + await fetchEvents(); + }; + + const filteredEvents = events.filter((event) => { + if (filter === "all") return true; + // Event status filters + if (filter === "upcoming" || filter === "ongoing" || filter === "completed") { + return event.event_status === filter; + } + // Registration status filter + const isOpen = typeof event.registrationstatus === 'string' + ? event.registrationstatus === 'open' + : event.registrationstatus; + if (filter === "open") return isOpen; + // Category filters + return event.category === filter; + }); + + const categories = ["all", "upcoming", "ongoing", "completed", "open", "workshop", "hackathon", "seminar", "competition", "other"]; + + return ( + <> + {selectedEvent && ( + <> + + + + )} + +
+ {/* Header */} +
+
+
+

+ Events +

+

+ Discover and register for events organized by COC +

+
+
+
+ +
+ {/* Filters */} +
+ {categories.map((cat) => ( + + ))} +
+ + {/* Events Grid */} + {loading ? ( +
+ {[1, 2, 3, 4, 5, 6].map((i) => ( +
+ ))} +
+ ) : filteredEvents.length === 0 ? ( +
+ +

No events found

+

+ {filter === "all" + ? "There are no events available at the moment." + : `No ${filter} events found. Try a different filter.`} +

+
+ ) : ( +
+ {filteredEvents.map((event, index) => { + const isRegistered = registeredEventIds.has(event.id); + const registrationstatus = typeof event.registrationstatus === 'string' + ? event.registrationstatus.toLowerCase() + : 'closed'; + + const getRegistrationBadge = () => { + switch (registrationstatus) { + case 'open': + return { + className: "border-green-500/50 text-green-400", + icon: , + text: "Open" + }; + case 'upcoming': + return { + className: "border-blue-500/50 text-blue-400", + icon: , + text: "Coming Soon" + }; + case 'closed': + default: + return { + className: "border-red-500/50 text-red-400", + icon: , + text: "Closed" + }; + } + }; + + const getEventStatusBadge = () => { + switch (event.event_status) { + case 'upcoming': + return { + className: "border-cyan-500/50 text-cyan-400 bg-cyan-500/10", + text: "Upcoming" + }; + case 'ongoing': + return { + className: "border-yellow-500/50 text-yellow-400 bg-yellow-500/10", + text: "Ongoing" + }; + case 'completed': + return { + className: "border-neutral-500/50 text-neutral-400 bg-neutral-500/10", + text: "Completed" + }; + case 'cancelled': + return { + className: "border-red-500/50 text-red-400 bg-red-500/10", + text: "Cancelled" + }; + default: + return null; + } + }; + + const registrationBadge = getRegistrationBadge(); + const eventStatusBadge = getEventStatusBadge(); + const isCompleted = event.event_status === 'completed'; + const isCancelled = event.event_status === 'cancelled'; + + return ( + + + {/* Event Image */} + {event.imageurl && ( +
+ {event.title} + {/* Event Status Badge on Image */} + {eventStatusBadge && ( +
+ + {eventStatusBadge.text} + +
+ )} +
+ )} + + +
+
+ {/* Show event status if no image */} + {!event.imageurl && eventStatusBadge && ( + + {eventStatusBadge.text} + + )} + + {registrationBadge.icon} + {registrationBadge.text} + + {isRegistered && ( + + + Registered + + )} +
+ + {event.category} + +
+ {event.title} + + {event.description.replace(/[#*`]/g, '')} + +
+ +
+
+ + + {event.time} + {event.date ? ( + · {format(parseISO(event.date), 'MMM dd, yyyy')} + ) : null} + +
+
+ + {event.location} +
+
+ + {event.participantcount} / {event.maxparticipants} {event.team_event ? 'participants' : 'registered'} +
+ {event.team_event && ( +
+ + Team Event ({event.min_team_size}-{event.max_team_size} members) +
+ )} + + {event.tags && event.tags.length > 0 && ( +
+ {event.tags.slice(0, 3).map((tag, i) => ( + + {tag} + + ))} + {event.tags.length > 3 && ( + + +{event.tags.length - 3} + + )} +
+ )} +
+ +
+ + +
+
+
+
+ )} + )} +
+ )} +
+
+ + ); +} diff --git a/app/dashboard/layout.tsx b/app/dashboard/layout.tsx index 19ff1c8..baa5958 100644 --- a/app/dashboard/layout.tsx +++ b/app/dashboard/layout.tsx @@ -1,20 +1,46 @@ + "use client"; +import { useState } from "react"; import { Sidebar } from "@/components/Sidebar"; +import { Menu } from "lucide-react"; export default function DashboardLayout({ children, }: { children: React.ReactNode; }) { + const [sidebarOpen, setSidebarOpen] = useState(false); + + // Callback to close sidebar only on mobile + const handleSidebarLinkClick = () => { + if (window.innerWidth < 1024) setSidebarOpen(false); + }; + return ( -
-
+
+ {/* Sidebar for large screens */} +
- -
-
+ + {/* Sidebar popup for small screens */} +
+ +
+ + + +
+
{children}
diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index dab6750..28c6982 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -1,86 +1,407 @@ "use client"; import { useSession } from "next-auth/react"; -import { usePathname } from "next/navigation"; +import { useState, useEffect, useMemo } from "react"; import { motion } from "framer-motion"; import { Card } from "@/components/ui/card"; -// import { ScrollArea } from "@/components/ui/scroll-area"; -// import ResourceTable from "@/components/ResourceTable"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; import { domains } from "@/config/navigation"; import Link from "next/link"; import { cn } from "@/lib/utils"; +import { LayoutGrid, Calendar, MapPin, Users, Clock, ChevronLeft, ChevronRight, CalendarIcon as CalendarIconLucide } from "lucide-react"; +import { getEvents } from "@/app/actions/events"; +import { getParticipants } from "@/app/actions/participants"; +import { getCurrentUserProfile } from "@/app/actions/users"; +import { EventWithStats } from "@/types/events"; +import { format, startOfMonth, endOfMonth, isSameMonth, addMonths, subMonths, parseISO } from "date-fns"; export default function Dashboard() { const { data: session } = useSession(); - const pathname = usePathname(); - const currentDomain = domains.find( - domain => `/dashboard/${domain.resources}` === pathname - ); + const [events, setEvents] = useState([]); + const [loading, setLoading] = useState(true); + const [currentMonth, setCurrentMonth] = useState(new Date()); + const [registeredEventIds, setRegisteredEventIds] = useState>(new Set()); + + useEffect(() => { + fetchEvents(); + fetchUserRegistrations(); + }, []); + + const fetchUserRegistrations = async () => { + try { + // Get current user's UID + const userResult = await getCurrentUserProfile(); + if (!userResult.success || !userResult.data) { + console.error("Failed to get user profile"); + return; + } + + const userId = userResult.data.uid; + + // Fetch only this user's registrations + const result = await getParticipants({ userId }); + + if (result.success && result.data) { + const eventIds = new Set( + result.data.map((p: any) => p.event_id as string) + ); + setRegisteredEventIds(eventIds); + } + } catch (error) { + console.error("Error fetching registrations:", error); + } + }; + + const fetchEvents = async () => { + try { + setLoading(true); + const result = await getEvents(); + + if (result.success && result.data) { + // Filter to show only upcoming and ongoing events (event_status is auto-updated) + const upcomingEvents = result.data.filter((event: any) => + event.event_status === 'upcoming' || event.event_status === 'ongoing' + ); + + const transformedEvents = upcomingEvents.map((event: any) => ({ + id: event.id, + title: event.title, + description: event.description, + date: event.date || new Date().toISOString().split('T')[0], + time: event.time, + location: event.location, + maxParticipants: event.maxparticipants, + registrationstatus: event.registrationstatus || 'upcoming', + category: event.category, + organizer: event.organizer, + imageUrl: event.imageurl, + tags: event.tags || [], + requirements: event.requirements || [], + createdAt: event.created_at, + updatedAt: event.updated_at, + participantCount: event.participantcount || 0, + teamEvent: event.team_event || false, + maxTeamSize: event.max_team_size || 1, + minTeamSize: event.min_team_size || 1, + eventStatus: event.event_status, + isFeatured: event.is_featured, + stats: event.stats || { + total: 0, + confirmed: 0, + attended: 0, + cancelled: 0 + } + })); + setEvents(transformedEvents); + } + } catch (error) { + console.error('Error fetching events:', error); + } finally { + setLoading(false); + } + }; + + // Get events for current month + const monthEvents = useMemo(() => { + return events.filter(event => { + try { + const eventDate = parseISO(event.date); + return isSameMonth(eventDate, currentMonth); + } catch { + return false; + } + }).sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()); + }, [events, currentMonth]); + + // Count user's registered events in the current month + const userRegisteredCount = useMemo(() => { + return monthEvents.filter(event => registeredEventIds.has(event.id)).length; + }, [monthEvents, registeredEventIds]); + + const getCategoryColor = (category: string) => { + const colors: Record = { + workshop: "bg-blue-500/20 text-blue-300 border-blue-500/30", + hackathon: "bg-purple-500/20 text-purple-300 border-purple-500/30", + seminar: "bg-green-500/20 text-green-300 border-green-500/30", + competition: "bg-red-500/20 text-red-300 border-red-500/30", + webinar: "bg-cyan-500/20 text-cyan-300 border-cyan-500/30", + bootcamp: "bg-orange-500/20 text-orange-300 border-orange-500/30", + conference: "bg-pink-500/20 text-pink-300 border-pink-500/30", + networking: "bg-yellow-500/20 text-yellow-300 border-yellow-500/30", + "tech-talk": "bg-indigo-500/20 text-indigo-300 border-indigo-500/30", + "panel-discussion": "bg-teal-500/20 text-teal-300 border-teal-500/30", + "project-showcase": "bg-violet-500/20 text-violet-300 border-violet-500/30", + "coding-contest": "bg-rose-500/20 text-rose-300 border-rose-500/30", + "study-group": "bg-lime-500/20 text-lime-300 border-lime-500/30", + other: "bg-gray-500/20 text-gray-300 border-gray-500/30", + }; + return colors[category] || colors.other; + }; + + const getStatusColor = (status: string) => { + const colors: Record = { + open: "bg-emerald-500/20 text-emerald-300", + closed: "bg-red-500/20 text-red-300", + upcoming: "bg-blue-500/20 text-blue-300", + }; + return colors[status] || colors.upcoming; + }; return ( -
- - + + -

+

Welcome back, {session?.user?.name?.split(" ")[0]}!

-

- Explore {currentDomain?.name || "educational"} resources curated just for you. +

+ Your central hub for resources and events.

- - {domains.map((domain) => ( - - -
-
- + {/* Resources Card */} + +
+
+
+
+
-
-

- {domain.name} -

-

- {domain.description} -

-
- {domain.categories.map((category) => ( - - {category} - - ))} +

+ Browse Resources +

+
+
+ +

+ Explore curated educational resources across different domains +

+ + {/* Domain Resources Grid */} +
+ {domains.map((domain, index) => ( + + + +
+
+ +
+
+

+ {domain.name} +

+

+ {domain.description} +

+
+
+
+
+ + ))} +
+
+ + + {/* Events Calendar Card with Month List */} + +
+
+
+
+ +
+

+ Event Calendar +

+
+ + + +
+ + {/* Month Navigation */} +
+
+ + + {format(currentMonth, 'MMMM yyyy')} + + +
+ +
+ + {/* Events List */} +
+ {loading ? ( +
+
Loading events...
+
+ ) : monthEvents.length > 0 ? ( + monthEvents.map((event, index) => ( + window.location.href = '/dashboard/events'} + > +
+ {/* Event Image */} + {event.imageUrl && ( +
+ {event.title} +
+ )} + + {/* Event Content */} +
+
+
+

+ {event.title} +

+ + {event.category} + +
+
+
+ + {format(parseISO(event.date), 'MMM dd, yyyy')} + {format(parseISO(event.date), 'MMM dd')} +
+ {event.time && ( +
+ + {event.time} +
+ )} +
+
+
+ + {event.location} +
+ {event.maxParticipants && ( +
+ + + {event.participantCount}/{event.maxParticipants} + +
+ )} +
+
+
+ + {/* Status Badge */} +
+ + {event.registrationstatus} + +
+
+
+ )) + ) : ( +
+ +

No events for {format(currentMonth, 'MMMM yyyy')}

+
+ )} +
+ + {/* Month Summary */} + {!loading && monthEvents.length > 0 && ( +
+
+
+

{monthEvents.length}

+

Events

+
+
+

+ {monthEvents.filter(e => e.registrationstatus === 'open').length} +

+

Open

+
+
+

+ {userRegisteredCount} +

+

Registered

- - - ))} + )} +
+
); -} \ No newline at end of file +} diff --git a/app/dev-club/page.tsx b/app/dev-club/page.tsx index 94ba043..797a4d8 100644 --- a/app/dev-club/page.tsx +++ b/app/dev-club/page.tsx @@ -1,30 +1,33 @@ import Navbar from "@/components/Navbar"; -import { SectionHero } from "@/components/sections/SectionHero"; +import { HeroSection } from "@/components/sections/HeroSection"; import { FeaturesSectionDemo } from "@/components/sections/EventsSection"; import { Suspense } from "react"; export default function DevClubPage() { const badges = [ - { label: "Web Development", color: "bg-green-500/10 text-green-300" }, - { label: "Mobile Apps", color: "bg-emerald-500/10 text-emerald-300" }, - { label: "Cloud Computing", color: "bg-green-500/10 text-green-300" }, - { label: "DevOps", color: "bg-emerald-500/10 text-emerald-300" }, - { label: "System Design", color: "bg-green-500/10 text-green-300" }, - { label: "Full Stack", color: "bg-emerald-500/10 text-emerald-300" }, + { label: "Web Development", className: "bg-green-500/10 text-green-300" }, + { label: "Mobile Apps", className: "bg-emerald-500/10 text-emerald-300" }, + { label: "Cloud Computing", className: "bg-green-500/10 text-green-300" }, + { label: "DevOps", className: "bg-emerald-500/10 text-emerald-300" }, + { label: "System Design", className: "bg-green-500/10 text-green-300" }, + { label: "Full Stack", className: "bg-emerald-500/10 text-emerald-300" }, ]; return ( Loading...
}>
- + - - {/* Other sections will go here */} + + {/* Other sections will go here */} + {/* */} +
); diff --git a/app/eth-club/page.tsx b/app/eth-club/page.tsx index dee3856..6ee486d 100644 --- a/app/eth-club/page.tsx +++ b/app/eth-club/page.tsx @@ -1,26 +1,32 @@ import Navbar from "@/components/Navbar"; -import { SectionHero } from "@/components/sections/SectionHero"; +import { HeroSection } from "@/components/sections/HeroSection"; +import { TeamMembers } from "@/components/eth-club/TeamMembers"; +import { Events } from "@/components/eth-club/Events"; +import { Suspense } from "react"; export default function EthClubPage() { const badges = [ - { label: "Web Development", color: "bg-green-500/10 text-green-300" }, - { label: "Mobile Apps", color: "bg-emerald-500/10 text-emerald-300" }, - { label: "Cloud Computing", color: "bg-green-500/10 text-green-300" }, - { label: "DevOps", color: "bg-emerald-500/10 text-emerald-300" }, - { label: "System Design", color: "bg-green-500/10 text-green-300" }, - { label: "Full Stack", color: "bg-emerald-500/10 text-emerald-300" }, + { label: "Blockchain", className: "bg-green-500/10 text-green-300" }, + { label: "Smart Contracts", className: "bg-emerald-500/10 text-emerald-300" }, + { label: "Web3", className: "bg-green-500/10 text-green-300" }, + { label: "DeFi", className: "bg-emerald-500/10 text-emerald-300" }, + { label: "NFTs", className: "bg-green-500/10 text-green-300" }, + { label: "Solidity", className: "bg-emerald-500/10 text-emerald-300" }, ]; return ( -
- - - {/* Other sections will go here */} -
+ Loading...
}> +
+ + + + +
+ ); } \ No newline at end of file diff --git a/app/globals.css b/app/globals.css index a26f983..0b131c0 100644 --- a/app/globals.css +++ b/app/globals.css @@ -115,6 +115,14 @@ body { border-radius: 4px; } +html, body, #__next { + height: 100%; + margin: 0; + background-color: #000000; + color: #ffffff; /* optional: text color */ +} + + /* For Firefox */ * { scrollbar-width: thin; diff --git a/app/layout.tsx b/app/layout.tsx index 4d6558d..0c7d18f 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -108,30 +108,31 @@ export default function RootLayout({ children: React.ReactNode; }>) { return ( - + -