From 20f405cab32802afa242fc74cf2ddaac8d8f5f9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Gierwia=C5=82o?= Date: Thu, 13 Nov 2025 21:18:15 +0100 Subject: [PATCH] feat: track event participation and show joined events first Backend: - Add EventParticipant model to track user-event participation - Create database migration for event_participants table - Record participation when user joins event chat via Socket.IO - Update GET /api/events to include isJoined flag for current user - Sort events: joined events first, then by start date - Add authenticate middleware to GET /api/events Frontend: - Replace mock events with real API data from backend - Add loading and error states to EventsPage - Display "Joined" badge on events user has joined - Highlight joined events with colored border - Show "Open chat" vs "Join chat" button text - Auto-refresh events list when navigating back When users join an event chat, this is now recorded in the database. Joined events appear at the top of the list with visual indicators. --- .../migration.sql | 24 ++++++ backend/prisma/schema.prisma | 19 +++++ backend/src/routes/events.js | 45 +++++++++-- backend/src/socket/index.js | 15 ++++ frontend/src/pages/EventsPage.jsx | 76 ++++++++++++++++--- 5 files changed, 164 insertions(+), 15 deletions(-) create mode 100644 backend/prisma/migrations/20251113201202_add_event_participants/migration.sql 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}

+ )}
))}