diff --git a/backend/prisma/migrations/20251113201202_add_event_participants/migration.sql b/backend/prisma/migrations/20251113201202_add_event_participants/migration.sql new file mode 100644 index 0000000..d815ea6 --- /dev/null +++ b/backend/prisma/migrations/20251113201202_add_event_participants/migration.sql @@ -0,0 +1,24 @@ +-- CreateTable +CREATE TABLE "event_participants" ( + "id" SERIAL NOT NULL, + "user_id" INTEGER NOT NULL, + "event_id" INTEGER NOT NULL, + "joined_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "event_participants_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "event_participants_user_id_idx" ON "event_participants"("user_id"); + +-- CreateIndex +CREATE INDEX "event_participants_event_id_idx" ON "event_participants"("event_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "event_participants_user_id_event_id_key" ON "event_participants"("user_id", "event_id"); + +-- AddForeignKey +ALTER TABLE "event_participants" ADD CONSTRAINT "event_participants_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "event_participants" ADD CONSTRAINT "event_participants_event_id_fkey" FOREIGN KEY ("event_id") REFERENCES "events"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 3b7584a..d010229 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -53,6 +53,7 @@ model User { matchesAsUser2 Match[] @relation("MatchUser2") ratingsGiven Rating[] @relation("RaterRatings") ratingsReceived Rating[] @relation("RatedRatings") + eventParticipants EventParticipant[] @@map("users") } @@ -72,6 +73,7 @@ model Event { // Relations chatRooms ChatRoom[] matches Match[] + participants EventParticipant[] @@map("events") } @@ -153,3 +155,20 @@ model Rating { @@index([ratedId]) @@map("ratings") } + +// Event participants (tracks which users joined which events) +model EventParticipant { + id Int @id @default(autoincrement()) + userId Int @map("user_id") + eventId Int @map("event_id") + joinedAt DateTime @default(now()) @map("joined_at") + + // Relations + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + event Event @relation(fields: [eventId], references: [id], onDelete: Cascade) + + @@unique([userId, eventId]) + @@index([userId]) + @@index([eventId]) + @@map("event_participants") +} diff --git a/backend/src/routes/events.js b/backend/src/routes/events.js index a31d751..2be5077 100644 --- a/backend/src/routes/events.js +++ b/backend/src/routes/events.js @@ -5,12 +5,12 @@ const { authenticate } = require('../middleware/auth'); const router = express.Router(); // GET /api/events - List all events -router.get('/', async (req, res, next) => { +router.get('/', authenticate, async (req, res, next) => { try { + const userId = req.user.id; + + // Fetch all events with participation info const events = await prisma.event.findMany({ - orderBy: { - startDate: 'asc', - }, select: { id: true, name: true, @@ -21,13 +21,46 @@ router.get('/', async (req, res, next) => { participantsCount: true, description: true, createdAt: true, + participants: { + where: { + userId: userId, + }, + select: { + joinedAt: true, + }, + }, }, }); + // Transform data and add isJoined flag + const eventsWithJoinedStatus = events.map(event => ({ + id: event.id, + name: event.name, + location: event.location, + startDate: event.startDate, + endDate: event.endDate, + worldsdcId: event.worldsdcId, + participantsCount: event.participantsCount, + description: event.description, + createdAt: event.createdAt, + isJoined: event.participants.length > 0, + joinedAt: event.participants[0]?.joinedAt || null, + })); + + // Sort: joined events first, then by start date + eventsWithJoinedStatus.sort((a, b) => { + // First, sort by joined status (joined events first) + if (a.isJoined && !b.isJoined) return -1; + if (!a.isJoined && b.isJoined) return 1; + + // Then sort by start date + return new Date(a.startDate) - new Date(b.startDate); + }); + res.json({ success: true, - count: events.length, - data: events, + count: eventsWithJoinedStatus.length, + data: eventsWithJoinedStatus, }); } catch (error) { next(error); diff --git a/backend/src/socket/index.js b/backend/src/socket/index.js index e2c5264..ba87a6b 100644 --- a/backend/src/socket/index.js +++ b/backend/src/socket/index.js @@ -61,6 +61,21 @@ function initializeSocket(httpServer) { socket.currentEventRoom = roomName; socket.currentEventId = eventId; + // Record event participation in database + await prisma.eventParticipant.upsert({ + where: { + userId_eventId: { + userId: socket.user.id, + eventId: parseInt(eventId), + }, + }, + update: {}, // Don't update anything if already exists + create: { + userId: socket.user.id, + eventId: parseInt(eventId), + }, + }); + // Add user to active users if (!activeUsers.has(eventId)) { activeUsers.set(eventId, new Set()); diff --git a/frontend/src/pages/EventsPage.jsx b/frontend/src/pages/EventsPage.jsx index 9d23bae..ebfbc9c 100644 --- a/frontend/src/pages/EventsPage.jsx +++ b/frontend/src/pages/EventsPage.jsx @@ -1,15 +1,61 @@ +import { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import Layout from '../components/layout/Layout'; -import { mockEvents } from '../mocks/events'; -import { Calendar, MapPin, Users } from 'lucide-react'; +import { eventsAPI } from '../services/api'; +import { Calendar, MapPin, Users, Loader2, CheckCircle } from 'lucide-react'; const EventsPage = () => { const navigate = useNavigate(); + const [events, setEvents] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(''); + + useEffect(() => { + const fetchEvents = async () => { + try { + setLoading(true); + const data = await eventsAPI.getAll(); + setEvents(data); + } catch (err) { + setError('Failed to load events'); + console.error('Error loading events:', err); + } finally { + setLoading(false); + } + }; + + fetchEvents(); + }, []); const handleJoinEvent = (eventId) => { navigate(`/events/${eventId}/chat`); }; + if (loading) { + return ( + +
+
+ +

Loading events...

+
+
+
+ ); + } + + if (error) { + return ( + +
+
+ {error} +
+
+
+ ); + } + return (
@@ -17,12 +63,22 @@ const EventsPage = () => {

Join an event and start connecting with other dancers

- {mockEvents.map((event) => ( + {events.map((event) => (
-

{event.name}

+
+

{event.name}

+ {event.isJoined && ( + + + Joined + + )} +
@@ -32,22 +88,24 @@ const EventsPage = () => {
- {new Date(event.start_date).toLocaleDateString('en-US')} - {new Date(event.end_date).toLocaleDateString('en-US')} + {new Date(event.startDate).toLocaleDateString('en-US')} - {new Date(event.endDate).toLocaleDateString('en-US')}
- {event.participants_count} participants + {event.participantsCount} participants
-

{event.description}

+ {event.description && ( +

{event.description}

+ )}
))}