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.
This commit is contained in:
Radosław Gierwiało
2025-11-13 21:18:15 +01:00
parent 897d6e61b3
commit 20f405cab3
5 changed files with 164 additions and 15 deletions

View File

@@ -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;

View File

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

View File

@@ -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);

View File

@@ -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());