Skip to content

Commit b5cbe03

Browse files
committed
feat: add featured events route
1 parent 9fe0dfe commit b5cbe03

File tree

13 files changed

+436
-93
lines changed

13 files changed

+436
-93
lines changed

apps/rpc/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
"@dotkomonline/utils": "workspace:*",
3939
"@fastify/cors": "^11.0.0",
4040
"@opentelemetry/api": "^1.9.0",
41-
"@prisma/client": "^6.8.2",
41+
"@prisma/client": "^6.19.0",
4242
"@sentry/node": "^9.24.0",
4343
"@trpc/server": "11.4.4",
4444
"auth0": "^4.23.1",

apps/rpc/src/modules/event/event-repository.ts

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import type { DBHandle } from "@dotkomonline/db"
2+
import { findFeaturedEvents } from "@dotkomonline/db"
23
import {
34
type AttendanceId,
5+
type BaseEvent,
6+
BaseEventSchema,
47
type CompanyId,
58
type DeregisterReason,
69
DeregisterReasonSchema,
@@ -41,6 +44,8 @@ const INCLUDE_COMPANY_AND_GROUPS = {
4144
export interface EventRepository {
4245
create(handle: DBHandle, data: EventWrite): Promise<Event>
4346
update(handle: DBHandle, eventId: EventId, data: Partial<EventWrite>): Promise<Event>
47+
updateEventAttendance(handle: DBHandle, eventId: EventId, attendanceId: AttendanceId): Promise<Event>
48+
updateEventParent(handle: DBHandle, eventId: EventId, parentEventId: EventId | null): Promise<Event>
4449
/**
4550
* Soft-delete an event by setting its status to "DELETED".
4651
*/
@@ -81,15 +86,17 @@ export interface EventRepository {
8186
page: Pageable
8287
): Promise<Event[]>
8388
findByParentEventId(handle: DBHandle, parentEventId: EventId): Promise<Event[]>
89+
findEventsWithUnansweredFeedbackFormByUserId(handle: DBHandle, userId: UserId): Promise<Event[]>
90+
findManyDeregisterReasonsWithEvent(handle: DBHandle, page: Pageable): Promise<DeregisterReasonWithEvent[]>
91+
// This cannot use `Pageable` due to raw query needing numerical offset and not cursor based pagination
92+
findFeaturedEvents(handle: DBHandle, offset: number, limit: number): Promise<BaseEvent[]>
93+
8494
addEventHostingGroups(handle: DBHandle, eventId: EventId, hostingGroupIds: Set<GroupId>): Promise<void>
85-
addEventCompanies(handle: DBHandle, eventId: EventId, companyIds: Set<CompanyId>): Promise<void>
8695
deleteEventHostingGroups(handle: DBHandle, eventId: EventId, hostingGroupIds: Set<GroupId>): Promise<void>
96+
addEventCompanies(handle: DBHandle, eventId: EventId, companyIds: Set<CompanyId>): Promise<void>
8797
deleteEventCompanies(handle: DBHandle, eventId: EventId, companyIds: Set<CompanyId>): Promise<void>
88-
updateEventAttendance(handle: DBHandle, eventId: EventId, attendanceId: AttendanceId): Promise<Event>
89-
updateEventParent(handle: DBHandle, eventId: EventId, parentEventId: EventId | null): Promise<Event>
90-
findEventsWithUnansweredFeedbackFormByUserId(handle: DBHandle, userId: UserId): Promise<Event[]>
98+
9199
createDeregisterReason(handle: DBHandle, data: DeregisterReasonWrite): Promise<DeregisterReason>
92-
findManyDeregisterReasonsWithEvent(handle: DBHandle, page: Pageable): Promise<DeregisterReasonWithEvent[]>
93100
}
94101

95102
export function getEventRepository(): EventRepository {
@@ -310,6 +317,25 @@ export function getEventRepository(): EventRepository {
310317
)
311318
},
312319

320+
async findFeaturedEvents(handle, offset, limit) {
321+
// Events will primarily be ranked by their type in the following order (lower number is higher ranking):
322+
// 1. GENERAL_ASSEMBLY
323+
// 2. COMPANY, ACADEMIC
324+
// 3. SOCIAL, INTERNAL, OTHER, WELCOME
325+
//
326+
// Within each bucket they will be ranked like this:
327+
// 1. Event in future, registration open and not full AND attendance capacity is limited (>0)
328+
// 2. Event in future, registration not started yet (attendance capacity does not matter)
329+
// 3. Event in future, no attendance registration OR attendance capacity is unlimited (=0)
330+
// 4. Event in future, registration full (registration status does not matter)
331+
//
332+
// Past events are not featured. We would rather have no featured events than "stale" events.
333+
334+
const events = await handle.$queryRawTyped(findFeaturedEvents(offset, limit))
335+
336+
return parseOrReport(BaseEventSchema.array(), events)
337+
},
338+
313339
async addEventHostingGroups(handle, eventId, hostingGroupIds) {
314340
await handle.eventHostingGroup.createMany({
315341
data: hostingGroupIds

apps/rpc/src/modules/event/event-router.ts

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import type { PresignedPost } from "@aws-sdk/s3-presigned-post"
22
import {
3+
AttendanceSchema,
34
AttendanceWriteSchema,
5+
BaseEventSchema,
46
CompanySchema,
57
EventFilterQuerySchema,
68
EventSchema,
@@ -153,7 +155,12 @@ const allEventsProcedure = procedure
153155
export type AllByAttendingUserIdInput = inferProcedureInput<typeof allByAttendingUserIdProcedure>
154156
export type AllByAttendingUserIdOutput = inferProcedureOutput<typeof allByAttendingUserIdProcedure>
155157
const allByAttendingUserIdProcedure = procedure
156-
.input(BasePaginateInputSchema.extend({ filter: EventFilterQuerySchema.optional(), id: UserSchema.shape.id }))
158+
.input(
159+
BasePaginateInputSchema.extend({
160+
filter: EventFilterQuerySchema.optional(),
161+
id: UserSchema.shape.id,
162+
})
163+
)
157164
.output(
158165
z.object({
159166
items: EventWithAttendanceSchema.array(),
@@ -172,7 +179,7 @@ const allByAttendingUserIdProcedure = procedure
172179

173180
const eventsWithAttendance = events.map((event) => ({
174181
event,
175-
attendance: attendances.find((attendance) => attendance.id === event.attendanceId) || null,
182+
attendance: attendances.find((attendance) => attendance.id === event.attendanceId) ?? null,
176183
}))
177184

178185
return {
@@ -244,7 +251,7 @@ const findChildEventsProcedure = procedure
244251
)
245252
return events.map((event) => ({
246253
event,
247-
attendance: attendances.find((attendance) => attendance.id === event.attendanceId) || null,
254+
attendance: attendances.find((attendance) => attendance.id === event.attendanceId) ?? null,
248255
}))
249256
})
250257

@@ -290,6 +297,33 @@ const findManyDeregisterReasonsWithEventProcedure = procedure
290297
}
291298
})
292299

300+
export type FindFeaturedEventsInput = inferProcedureInput<typeof findFeaturedEventsProcedure>
301+
export type FindFeaturedEventsOutput = inferProcedureOutput<typeof findFeaturedEventsProcedure>
302+
const findFeaturedEventsProcedure = procedure
303+
.input(
304+
z.object({
305+
offset: z.number().min(0).default(0),
306+
limit: z.number().min(1).max(100).default(10),
307+
})
308+
)
309+
.output(z.object({ event: BaseEventSchema, attendance: AttendanceSchema.nullable() }).array())
310+
.use(withDatabaseTransaction())
311+
.query(async ({ input, ctx }) => {
312+
const events = await ctx.eventService.findFeaturedEvents(ctx.handle, input.offset, input.limit)
313+
314+
const attendances = await ctx.attendanceService.getAttendancesByIds(
315+
ctx.handle,
316+
events.map((item) => item.attendanceId).filter((id) => id !== null)
317+
)
318+
319+
const eventsWithAttendance = events.map((event) => ({
320+
event,
321+
attendance: attendances.find((attendance) => attendance.id === event.attendanceId) ?? null,
322+
}))
323+
324+
return eventsWithAttendance
325+
})
326+
293327
export type CreateFileUploadInput = inferProcedureInput<typeof createFileUploadProcedure>
294328
export type CreateFileUploadOutput = inferProcedureOutput<typeof createFileUploadProcedure>
295329
const createFileUploadProcedure = procedure

apps/rpc/src/modules/event/event-service.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { DBHandle } from "@dotkomonline/db"
44
import { getLogger } from "@dotkomonline/logger"
55
import type {
66
AttendanceId,
7+
BaseEvent,
78
CompanyId,
89
DeregisterReason,
910
DeregisterReasonWithEvent,
@@ -45,6 +46,7 @@ export interface EventService {
4546
findByParentEventId(handle: DBHandle, parentEventId: EventId): Promise<Event[]>
4647
findEventById(handle: DBHandle, eventId: EventId): Promise<Event | null>
4748
findEventsWithUnansweredFeedbackFormByUserId(handle: DBHandle, userId: UserId): Promise<Event[]>
49+
findFeaturedEvents(handle: DBHandle, offset: number, limit: number): Promise<BaseEvent[]>
4850
/**
4951
* Get an event by its id
5052
*
@@ -98,6 +100,10 @@ export function getEventService(
98100
return await eventRepository.findEventsWithUnansweredFeedbackFormByUserId(handle, userId)
99101
},
100102

103+
async findFeaturedEvents(handle, offset, limit) {
104+
return await eventRepository.findFeaturedEvents(handle, offset, limit)
105+
},
106+
101107
async getEventById(handle, eventId) {
102108
const event = await eventRepository.findById(handle, eventId)
103109
if (!event) {

apps/web/src/app/page.tsx

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import type { Attendance, Event, EventWithAttendance, UserId } from "@dotkomonli
1010
import { RichText, cn } from "@dotkomonline/ui"
1111
import { Text, Tilt, Title } from "@dotkomonline/ui"
1212
import { Button } from "@dotkomonline/ui"
13-
import { getCurrentUTC, slugify } from "@dotkomonline/utils"
13+
import { slugify } from "@dotkomonline/utils"
1414
import { IconArrowRight, IconCalendarEvent } from "@tabler/icons-react"
1515
import { formatDate } from "date-fns"
1616
import { nb } from "date-fns/locale"
@@ -20,17 +20,8 @@ import type { FC } from "react"
2020
export default async function App() {
2121
const [session, isStaff] = await Promise.all([auth.getServerSession(), server.user.isStaff.query()])
2222

23-
const { items: events } = await server.event.all.query({
24-
take: 3,
25-
filter: {
26-
byEndDate: {
27-
max: null,
28-
min: getCurrentUTC(),
29-
},
30-
excludingOrganizingGroup: ["velkom"],
31-
excludingType: isStaff ? [] : undefined,
32-
orderBy: "asc",
33-
},
23+
const events = await server.event.findFeaturedEvents.query({
24+
limit: 3,
3425
})
3526

3627
const featuredEvent = events[0] ?? null

package.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,12 @@
2020
"test:it": "pnpm run -r test:it",
2121
"dev": "pnpm -rc --parallel -F @dotkomonline/ui -F @dotkomonline/web -F @dotkomonline/dashboard -F @dotkomonline/rpc -F @dotkomonline/invoicification -F @dotkomonline/rif exec doppler run --preserve-env pnpm run dev",
2222
"dev:web": "pnpm -rc --parallel -F @dotkomonline/ui -F @dotkomonline/web -F @dotkomonline/dashboard -F @dotkomonline/rpc exec doppler run --preserve-env pnpm run dev",
23-
"prisma": "pnpm -F @dotkomonline/db prisma",
24-
"generate": "pnpm -F @dotkomonline/db generate",
25-
"migrate:dev": "pnpm -F @dotkomonline/db migrate",
26-
"migrate:dev-with-fixtures": "pnpm -F @dotkomonline/db prisma migrate dev && pnpm -F @dotkomonline/db apply-fixtures",
27-
"migrate:deploy": "pnpm -F @dotkomonline/db prisma migrate deploy",
28-
"migrate:deploy-with-fixtures": "pnpm -F @dotkomonline/db prisma migrate deploy && pnpm -F @dotkomonline/db apply-fixtures",
23+
"prisma": "doppler run --project monoweb-rpc -- pnpm -F @dotkomonline/db prisma",
24+
"generate": "doppler run --project monoweb-rpc -- pnpm -F @dotkomonline/db generate",
25+
"migrate:dev": "doppler run --project monoweb-rpc -- pnpm -F @dotkomonline/db migrate",
26+
"migrate:dev-with-fixtures": "doppler run --project monoweb-rpc -- pnpm -F @dotkomonline/db prisma migrate dev && pnpm -F @dotkomonline/db apply-fixtures",
27+
"migrate:deploy": "doppler run --project monoweb-rpc -- pnpm -F @dotkomonline/db prisma migrate deploy",
28+
"migrate:deploy-with-fixtures": "doppler run --project monoweb-rpc -- pnpm -F @dotkomonline/db prisma migrate deploy && pnpm -F @dotkomonline/db apply-fixtures",
2929
"migrate:staging": "doppler run --config stg --project monoweb-rpc -- pnpm migrate:deploy",
3030
"migrate:prod": "doppler run --config prd --project monoweb-rpc -- pnpm migrate:deploy",
3131
"storybook": "pnpm run -r --filter=storybook storybook",

packages/db/package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,14 @@
2323
"type-check": "tsc --noEmit",
2424
"prisma": "prisma",
2525
"migrate": "prisma migrate dev",
26-
"generate": "prisma generate",
27-
"postinstall": "prisma generate",
26+
"generate": "prisma generate --sql",
27+
"postinstall": "prisma generate --sql",
2828
"apply-fixtures": "tsx src/fixtures.ts"
2929
},
3030
"dependencies": {
3131
"@dotkomonline/logger": "workspace:*",
3232
"@dotkomonline/utils": "workspace:*",
33-
"@prisma/client": "^6.8.2",
33+
"@prisma/client": "^6.19.0",
3434
"@testcontainers/postgresql": "^11.5.1",
3535
"date-fns": "^4.1.0",
3636
"pg": "^8.16.0",
@@ -44,7 +44,7 @@
4444
"@types/node": "22.18.6",
4545
"@types/pg": "8.15.6",
4646
"common-tags": "1.8.2",
47-
"prisma": "6.8.2",
47+
"prisma": "6.19.0",
4848
"tsx": "4.20.6",
4949
"typescript": "5.9.3"
5050
}

packages/db/prisma.config.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { defineConfig } from "prisma/config"
2+
3+
if (process.env.DATABASE_URL === undefined) {
4+
throw new Error("Missing database url")
5+
}
6+
7+
export default defineConfig({
8+
schema: "./prisma/schema.prisma",
9+
typedSql: {
10+
path: "./prisma/sql",
11+
},
12+
})

packages/db/prisma/schema.prisma

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
generator client {
22
provider = "prisma-client-js"
3-
previewFeatures = ["relationJoins"]
3+
previewFeatures = ["relationJoins", "typedSql"]
44
}
55

66
generator zod {
@@ -223,13 +223,20 @@ enum EventStatus {
223223
}
224224

225225
enum EventType {
226-
SOCIAL @map("SOCIAL")
227-
ACADEMIC @map("ACADEMIC")
228-
COMPANY @map("COMPANY")
229-
// This is called "Generalforsamling" in Norwegian and happens twice a year.
226+
/// Generalforsamling
230227
GENERAL_ASSEMBLY @map("GENERAL_ASSEMBLY")
228+
/// Bedriftspresentasjon
229+
COMPANY @map("COMPANY")
230+
/// Kurs
231+
ACADEMIC @map("ACADEMIC")
232+
SOCIAL @map("SOCIAL")
233+
// These are for the rare occations we have events that are only open to committee members
231234
INTERNAL @map("INTERNAL")
232235
OTHER @map("OTHER")
236+
// These are for a committe called "velkom" and are special social events for new students.
237+
// These have a separate type because we have historically hid these from event lists to not
238+
// spam students that are not new with these events. In older versions of OnlineWeb these
239+
// were even treated as a completely separate event entity.
233240
WELCOME @map("WELCOME")
234241
235242
@@map("event_type")

0 commit comments

Comments
 (0)