From 1697384c0a896b47427555d8c6d466b447025f86 Mon Sep 17 00:00:00 2001 From: Alexander Wang Date: Mon, 18 Aug 2025 13:10:35 -0400 Subject: [PATCH 1/3] feat: api done for event cards --- apps/api/internal/api/api.go | 2 +- apps/api/internal/api/handlers/events.go | 26 +++- apps/api/internal/ctxutils/user.go | 38 ++++++ apps/api/internal/db/queries/events.sql | 31 ++++- apps/api/internal/db/repository/events.go | 16 ++- apps/api/internal/db/sqlc/events.sql.go | 153 ++++++++++++++++++++-- apps/api/internal/db/sqlc/querier.go | 5 +- apps/api/internal/ptr/bool.go | 6 + apps/api/internal/services/events.go | 21 ++- apps/api/internal/web/query.go | 36 +++++ apps/api/sqlc.yml | 3 + apps/web/src/lib/openapi/schema.d.ts | 17 ++- 12 files changed, 314 insertions(+), 40 deletions(-) create mode 100644 apps/api/internal/ctxutils/user.go create mode 100644 apps/api/internal/ptr/bool.go create mode 100644 apps/api/internal/web/query.go diff --git a/apps/api/internal/api/api.go b/apps/api/internal/api/api.go index 8a94202e..f41cadb9 100644 --- a/apps/api/internal/api/api.go +++ b/apps/api/internal/api/api.go @@ -76,7 +76,7 @@ func (api *API) setupRoutes(mw *mw.Middleware) { // Event routes api.Router.Route("/events", func(r chi.Router) { r.With(mw.Auth.RequireAuth, ensureSuperuser).Post("/", api.Handlers.Event.CreateEvent) - r.With(mw.Auth.RequireAuth).Get("/", api.Handlers.Event.GetAllEvents) + r.With(mw.Auth.RequireAuth).Get("/", api.Handlers.Event.GetEvents) r.Route("/{eventId}", func(r chi.Router) { r.With(mw.Auth.RequireAuth, ensureEventAdmin).Patch("/", api.Handlers.Event.UpdateEventById) r.With(mw.Auth.RequireAuth, ensureSuperuser).Delete("/", api.Handlers.Event.DeleteEventById) diff --git a/apps/api/internal/api/handlers/events.go b/apps/api/internal/api/handlers/events.go index 4affe84c..285bd24c 100644 --- a/apps/api/internal/api/handlers/events.go +++ b/apps/api/internal/api/handlers/events.go @@ -17,7 +17,9 @@ import ( "github.com/swamphacks/core/apps/api/internal/db/sqlc" "github.com/swamphacks/core/apps/api/internal/email" "github.com/swamphacks/core/apps/api/internal/parse" + "github.com/swamphacks/core/apps/api/internal/ptr" "github.com/swamphacks/core/apps/api/internal/services" + "github.com/swamphacks/core/apps/api/internal/web" ) type EventHandler struct { @@ -220,13 +222,31 @@ func (h *EventHandler) DeleteEventById(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNoContent) } -func (h *EventHandler) GetAllEvents(w http.ResponseWriter, r *http.Request) { - events, err := h.eventService.GetAllEvents(r.Context()) +func (h *EventHandler) GetEvents(w http.ResponseWriter, r *http.Request) { + // Parse query params + // inlcude_unpublished="true,false", default: false + queryParams := r.URL.Query() + includeUnpublished, err := web.ParseParamBoolean(queryParams, "include_unpublished", ptr.BoolToPtr(false)) if err != nil { - res.SendError(w, http.StatusInternalServerError, res.NewError("internal_err", "Something went wrong fetching events")) + res.SendError(w, http.StatusBadRequest, res.NewError("missing_fields", "Missing/malformed query. Available parameters: include_unpublished=true,false")) return } + events, err := h.eventService.GetEvents(r.Context(), *includeUnpublished) + if err == services.ErrMissingFields { + res.SendError(w, http.StatusBadRequest, res.NewError("missing_fields", "Missing/malformed query. Available parameteres: status=all,published")) + return + } + + if err == services.ErrMissingPerms { + res.SendError(w, http.StatusForbidden, res.NewError("forbidden", "You are forbidden from this resource.")) + return + } + + if err != nil { + res.SendError(w, http.StatusInternalServerError, res.NewError("internal_err", "Something went wrong encoding response")) + } + w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) if err := json.NewEncoder(w).Encode(events); err != nil { diff --git a/apps/api/internal/ctxutils/user.go b/apps/api/internal/ctxutils/user.go new file mode 100644 index 00000000..a04ff8c3 --- /dev/null +++ b/apps/api/internal/ctxutils/user.go @@ -0,0 +1,38 @@ +package ctxutils + +import ( + "context" + + "github.com/google/uuid" + mw "github.com/swamphacks/core/apps/api/internal/api/middleware" + "github.com/swamphacks/core/apps/api/internal/db/sqlc" +) + +// Takes in a context and returns whether the user has the superuser role +func IsSuperuser(ctx context.Context) bool { + userCtx, ok := ctx.Value(mw.UserContextKey).(*mw.UserContext) + if !ok { + return false + } + + return userCtx.Role == sqlc.AuthUserRoleSuperuser +} + +// Takes in a context and returns whether the user has the user role +func IsUser(ctx context.Context) bool { + userCtx, ok := ctx.Value(mw.UserContextKey).(*mw.UserContext) + if !ok { + return false + } + + return userCtx.Role == sqlc.AuthUserRoleUser +} + +func GetUserIdFromCtx(ctx context.Context) *uuid.UUID { + userCtx, ok := ctx.Value(mw.UserContextKey).(*mw.UserContext) + if !ok { + return nil + } + + return &userCtx.UserID +} diff --git a/apps/api/internal/db/queries/events.sql b/apps/api/internal/db/queries/events.sql index 1fb46303..d25965e4 100644 --- a/apps/api/internal/db/queries/events.sql +++ b/apps/api/internal/db/queries/events.sql @@ -56,8 +56,33 @@ SELECT * FROM event_roles WHERE user_id = @user_id::uuid AND event_id = @event_id::uuid; -- name: GetPublishedEvents :many -SELECT * FROM events -WHERE is_published = TRUE; +SELECT + e.*, + er.role AS event_role +FROM events e +LEFT JOIN event_roles AS er + ON er.event_id = e.id + AND er.user_id = $1 +WHERE e.is_published = TRUE +ORDER BY e.start_time ASC; -- name: GetAllEvents :many -SELECT * FROM events; +SELECT + e.*, + er.role AS event_role +FROM events e +LEFT JOIN event_roles AS er + ON er.event_id = e.id + AND er.user_id = $1 +ORDER BY e.start_time ASC; + +-- name: GetEventsWithRoles :many +SELECT + e.*, + er.role AS event_role +FROM events e +LEFT JOIN event_roles er + ON er.event_id = e.id + AND er.user_id = sqlc.narg(user_id) +WHERE (sqlc.arg(include_unpublished)::boolean IS TRUE OR e.is_published = TRUE) +ORDER BY e.start_time ASC; diff --git a/apps/api/internal/db/repository/events.go b/apps/api/internal/db/repository/events.go index ca98f47e..3da63069 100644 --- a/apps/api/internal/db/repository/events.go +++ b/apps/api/internal/db/repository/events.go @@ -74,13 +74,21 @@ func (r *EventRepository) DeleteEventById(ctx context.Context, id uuid.UUID) err return err } -func (r *EventRepository) GetAllEvents(ctx context.Context) (*[]sqlc.Event, error) { - events, err := r.db.Query.GetAllEvents(ctx) +func (r *EventRepository) GetAllEvents(ctx context.Context, userId uuid.UUID) (*[]sqlc.GetAllEventsRow, error) { + events, err := r.db.Query.GetAllEvents(ctx, userId) return &events, err } -func (r *EventRepository) GetPublishedEvents(ctx context.Context) (*[]sqlc.Event, error) { - events, err := r.db.Query.GetPublishedEvents(ctx) +func (r *EventRepository) GetPublishedEvents(ctx context.Context, userId uuid.UUID) (*[]sqlc.GetPublishedEventsRow, error) { + events, err := r.db.Query.GetPublishedEvents(ctx, userId) + return &events, err +} + +func (r *EventRepository) GetEventsWithRoles(ctx context.Context, userId *uuid.UUID, includeUnpublished bool) (*[]sqlc.GetEventsWithRolesRow, error) { + events, err := r.db.Query.GetEventsWithRoles(ctx, sqlc.GetEventsWithRolesParams{ + UserID: userId, + IncludeUnpublished: includeUnpublished, + }) return &events, err } diff --git a/apps/api/internal/db/sqlc/events.sql.go b/apps/api/internal/db/sqlc/events.sql.go index 75230692..d2b39012 100644 --- a/apps/api/internal/db/sqlc/events.sql.go +++ b/apps/api/internal/db/sqlc/events.sql.go @@ -106,18 +106,45 @@ func (q *Queries) DeleteEventById(ctx context.Context, id uuid.UUID) (int64, err } const getAllEvents = `-- name: GetAllEvents :many -SELECT id, name, description, location, location_url, max_attendees, application_open, application_close, rsvp_deadline, decision_release, start_time, end_time, website_url, is_published, created_at, updated_at FROM events +SELECT + e.id, e.name, e.description, e.location, e.location_url, e.max_attendees, e.application_open, e.application_close, e.rsvp_deadline, e.decision_release, e.start_time, e.end_time, e.website_url, e.is_published, e.created_at, e.updated_at, + er.role AS event_role +FROM events e +LEFT JOIN event_roles AS er + ON er.event_id = e.id + AND er.user_id = $1 +ORDER BY e.start_time ASC ` -func (q *Queries) GetAllEvents(ctx context.Context) ([]Event, error) { - rows, err := q.db.Query(ctx, getAllEvents) +type GetAllEventsRow struct { + ID uuid.UUID `json:"id"` + Name string `json:"name"` + Description *string `json:"description"` + Location *string `json:"location"` + LocationUrl *string `json:"location_url"` + MaxAttendees *int32 `json:"max_attendees"` + ApplicationOpen time.Time `json:"application_open"` + ApplicationClose time.Time `json:"application_close"` + RsvpDeadline *time.Time `json:"rsvp_deadline"` + DecisionRelease *time.Time `json:"decision_release"` + StartTime time.Time `json:"start_time"` + EndTime time.Time `json:"end_time"` + WebsiteUrl *string `json:"website_url"` + IsPublished *bool `json:"is_published"` + CreatedAt *time.Time `json:"created_at"` + UpdatedAt *time.Time `json:"updated_at"` + EventRole NullEventRoleType `json:"event_role"` +} + +func (q *Queries) GetAllEvents(ctx context.Context, userID uuid.UUID) ([]GetAllEventsRow, error) { + rows, err := q.db.Query(ctx, getAllEvents, userID) if err != nil { return nil, err } defer rows.Close() - items := []Event{} + items := []GetAllEventsRow{} for rows.Next() { - var i Event + var i GetAllEventsRow if err := rows.Scan( &i.ID, &i.Name, @@ -135,6 +162,7 @@ func (q *Queries) GetAllEvents(ctx context.Context) ([]Event, error) { &i.IsPublished, &i.CreatedAt, &i.UpdatedAt, + &i.EventRole, ); err != nil { return nil, err } @@ -197,20 +225,122 @@ func (q *Queries) GetEventRoleByIds(ctx context.Context, arg GetEventRoleByIdsPa return i, err } +const getEventsWithRoles = `-- name: GetEventsWithRoles :many +SELECT + e.id, e.name, e.description, e.location, e.location_url, e.max_attendees, e.application_open, e.application_close, e.rsvp_deadline, e.decision_release, e.start_time, e.end_time, e.website_url, e.is_published, e.created_at, e.updated_at, + er.role AS event_role +FROM events e +LEFT JOIN event_roles er + ON er.event_id = e.id + AND er.user_id = $1 +WHERE ($2::boolean IS TRUE OR e.is_published = TRUE) +ORDER BY e.start_time ASC +` + +type GetEventsWithRolesParams struct { + UserID *uuid.UUID `json:"user_id"` + IncludeUnpublished bool `json:"include_unpublished"` +} + +type GetEventsWithRolesRow struct { + ID uuid.UUID `json:"id"` + Name string `json:"name"` + Description *string `json:"description"` + Location *string `json:"location"` + LocationUrl *string `json:"location_url"` + MaxAttendees *int32 `json:"max_attendees"` + ApplicationOpen time.Time `json:"application_open"` + ApplicationClose time.Time `json:"application_close"` + RsvpDeadline *time.Time `json:"rsvp_deadline"` + DecisionRelease *time.Time `json:"decision_release"` + StartTime time.Time `json:"start_time"` + EndTime time.Time `json:"end_time"` + WebsiteUrl *string `json:"website_url"` + IsPublished *bool `json:"is_published"` + CreatedAt *time.Time `json:"created_at"` + UpdatedAt *time.Time `json:"updated_at"` + EventRole NullEventRoleType `json:"event_role"` +} + +func (q *Queries) GetEventsWithRoles(ctx context.Context, arg GetEventsWithRolesParams) ([]GetEventsWithRolesRow, error) { + rows, err := q.db.Query(ctx, getEventsWithRoles, arg.UserID, arg.IncludeUnpublished) + if err != nil { + return nil, err + } + defer rows.Close() + items := []GetEventsWithRolesRow{} + for rows.Next() { + var i GetEventsWithRolesRow + if err := rows.Scan( + &i.ID, + &i.Name, + &i.Description, + &i.Location, + &i.LocationUrl, + &i.MaxAttendees, + &i.ApplicationOpen, + &i.ApplicationClose, + &i.RsvpDeadline, + &i.DecisionRelease, + &i.StartTime, + &i.EndTime, + &i.WebsiteUrl, + &i.IsPublished, + &i.CreatedAt, + &i.UpdatedAt, + &i.EventRole, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const getPublishedEvents = `-- name: GetPublishedEvents :many -SELECT id, name, description, location, location_url, max_attendees, application_open, application_close, rsvp_deadline, decision_release, start_time, end_time, website_url, is_published, created_at, updated_at FROM events -WHERE is_published = TRUE +SELECT + e.id, e.name, e.description, e.location, e.location_url, e.max_attendees, e.application_open, e.application_close, e.rsvp_deadline, e.decision_release, e.start_time, e.end_time, e.website_url, e.is_published, e.created_at, e.updated_at, + er.role AS event_role +FROM events e +LEFT JOIN event_roles AS er + ON er.event_id = e.id + AND er.user_id = $1 +WHERE e.is_published = TRUE +ORDER BY e.start_time ASC ` -func (q *Queries) GetPublishedEvents(ctx context.Context) ([]Event, error) { - rows, err := q.db.Query(ctx, getPublishedEvents) +type GetPublishedEventsRow struct { + ID uuid.UUID `json:"id"` + Name string `json:"name"` + Description *string `json:"description"` + Location *string `json:"location"` + LocationUrl *string `json:"location_url"` + MaxAttendees *int32 `json:"max_attendees"` + ApplicationOpen time.Time `json:"application_open"` + ApplicationClose time.Time `json:"application_close"` + RsvpDeadline *time.Time `json:"rsvp_deadline"` + DecisionRelease *time.Time `json:"decision_release"` + StartTime time.Time `json:"start_time"` + EndTime time.Time `json:"end_time"` + WebsiteUrl *string `json:"website_url"` + IsPublished *bool `json:"is_published"` + CreatedAt *time.Time `json:"created_at"` + UpdatedAt *time.Time `json:"updated_at"` + EventRole NullEventRoleType `json:"event_role"` +} + +func (q *Queries) GetPublishedEvents(ctx context.Context, userID uuid.UUID) ([]GetPublishedEventsRow, error) { + rows, err := q.db.Query(ctx, getPublishedEvents, userID) if err != nil { return nil, err } defer rows.Close() - items := []Event{} + items := []GetPublishedEventsRow{} for rows.Next() { - var i Event + var i GetPublishedEventsRow if err := rows.Scan( &i.ID, &i.Name, @@ -228,6 +358,7 @@ func (q *Queries) GetPublishedEvents(ctx context.Context) ([]Event, error) { &i.IsPublished, &i.CreatedAt, &i.UpdatedAt, + &i.EventRole, ); err != nil { return nil, err } diff --git a/apps/api/internal/db/sqlc/querier.go b/apps/api/internal/db/sqlc/querier.go index 44d8d9ff..3027771c 100644 --- a/apps/api/internal/db/sqlc/querier.go +++ b/apps/api/internal/db/sqlc/querier.go @@ -28,14 +28,15 @@ type Querier interface { DeleteExpiredSession(ctx context.Context) error DeleteUser(ctx context.Context, id uuid.UUID) error GetActiveSessionUserInfo(ctx context.Context, id uuid.UUID) (GetActiveSessionUserInfoRow, error) - GetAllEvents(ctx context.Context) ([]Event, error) + GetAllEvents(ctx context.Context, userID uuid.UUID) ([]GetAllEventsRow, error) GetApplicationByUserAndEventID(ctx context.Context, arg GetApplicationByUserAndEventIDParams) (Application, error) GetByProviderAndAccountID(ctx context.Context, arg GetByProviderAndAccountIDParams) (AuthAccount, error) GetByUserID(ctx context.Context, userID uuid.UUID) ([]AuthAccount, error) GetEventByID(ctx context.Context, id uuid.UUID) (Event, error) GetEventRoleByIds(ctx context.Context, arg GetEventRoleByIdsParams) (EventRole, error) GetEventStaff(ctx context.Context, eventID uuid.UUID) ([]GetEventStaffRow, error) - GetPublishedEvents(ctx context.Context) ([]Event, error) + GetEventsWithRoles(ctx context.Context, arg GetEventsWithRolesParams) ([]GetEventsWithRolesRow, error) + GetPublishedEvents(ctx context.Context, userID uuid.UUID) ([]GetPublishedEventsRow, error) GetSessionByID(ctx context.Context, id uuid.UUID) (AuthSession, error) GetSessionsByUserID(ctx context.Context, userID uuid.UUID) ([]AuthSession, error) GetUserByEmail(ctx context.Context, email *string) (AuthUser, error) diff --git a/apps/api/internal/ptr/bool.go b/apps/api/internal/ptr/bool.go new file mode 100644 index 00000000..959dd4fe --- /dev/null +++ b/apps/api/internal/ptr/bool.go @@ -0,0 +1,6 @@ +package ptr + +// Takes a boolean and returns a pointer to that boolean +func BoolToPtr(b bool) *bool { + return &b +} diff --git a/apps/api/internal/services/events.go b/apps/api/internal/services/events.go index 2d05a21a..3c4a56eb 100644 --- a/apps/api/internal/services/events.go +++ b/apps/api/internal/services/events.go @@ -6,7 +6,7 @@ import ( "github.com/google/uuid" "github.com/rs/zerolog" - "github.com/swamphacks/core/apps/api/internal/api/middleware" + ctxu "github.com/swamphacks/core/apps/api/internal/ctxutils" "github.com/swamphacks/core/apps/api/internal/db/repository" "github.com/swamphacks/core/apps/api/internal/db/sqlc" ) @@ -18,6 +18,7 @@ var ( ErrFailedToDeleteEvent = errors.New("failed to delete event") ErrFailedToParseUUID = errors.New("failed to parse uuid") ErrMissingFields = errors.New("missing fields") + ErrMissingPerms = errors.New("missing perms") ) type EventService struct { @@ -97,20 +98,16 @@ func (s *EventService) DeleteEventById(ctx context.Context, id uuid.UUID) error return err } -func (s *EventService) GetAllEvents(ctx context.Context) (*[]sqlc.Event, error) { - // Check role, if role is none or user, return published events only - userCtx, ok := ctx.Value(middleware.UserContextKey).(*middleware.UserContext) - if !ok { - s.logger.Warn().Msg("Couldn't get user context") - return s.eventRepo.GetPublishedEvents(ctx) - } +func (s *EventService) GetEvents(ctx context.Context, includeUnpublished bool) (*[]sqlc.GetEventsWithRolesRow, error) { + isSuperuser := ctxu.IsSuperuser(ctx) + userId := ctxu.GetUserIdFromCtx(ctx) - //TODO: Replace with switch later - if userCtx.Role == sqlc.AuthUserRoleUser { - return s.eventRepo.GetPublishedEvents(ctx) + // Non-superusers can't get unpublished events + if !isSuperuser && includeUnpublished { + return nil, ErrMissingPerms } - return s.eventRepo.GetAllEvents(ctx) + return s.eventRepo.GetEventsWithRoles(ctx, userId, includeUnpublished) } diff --git a/apps/api/internal/web/query.go b/apps/api/internal/web/query.go new file mode 100644 index 00000000..e7afd0cd --- /dev/null +++ b/apps/api/internal/web/query.go @@ -0,0 +1,36 @@ +package web + +import ( + "errors" + "net/url" + "strconv" +) + +var ( + ErrMalformedField = errors.New("malformed field") +) + +// ParseParamBoolean parses a boolean query parameter from the given url.Values. +// If the parameter is missing or cannot be parsed as a boolean, it returns defaultVal. +// +// Parameters: +// - queryParams: the URL query parameters to read from +// - key: the name of the query parameter +// - defaultVal: pointer to the default boolean value to use if parsing fails or the parameter is absent +// +// Returns: +// - pointer to the parsed boolean value, or defaultVal if missing or invalid +// - error if field was malformed/not boolean +func ParseParamBoolean(queryParams url.Values, key string, defaultVal *bool) (*bool, error) { + v := queryParams.Get(key) + if v == "" { + return defaultVal, nil + } + + parsed, err := strconv.ParseBool(v) + if err != nil { + return nil, ErrMalformedField + } + + return &parsed, nil +} diff --git a/apps/api/sqlc.yml b/apps/api/sqlc.yml index 5dc30e91..b34bff57 100644 --- a/apps/api/sqlc.yml +++ b/apps/api/sqlc.yml @@ -26,5 +26,8 @@ sql: nullable: true go_type: type: "*time.Time" + - db_type: uuid + nullable: true + go_type: "*github.com/google/uuid.UUID" diff --git a/apps/web/src/lib/openapi/schema.d.ts b/apps/web/src/lib/openapi/schema.d.ts index 06061006..a81cf438 100644 --- a/apps/web/src/lib/openapi/schema.d.ts +++ b/apps/web/src/lib/openapi/schema.d.ts @@ -69,8 +69,8 @@ export interface paths { cookie?: never; }; /** - * Get all events - * @description Gets only published events for normal users, and all events for superusers + * Get events + * @description Gets events with a nullable event role for authenticated users. */ get: operations["get-events"]; put?: never; @@ -263,6 +263,12 @@ export interface components { * @enum {string} */ EventRole: "admin" | "staff" | "attendee" | "applicant"; + EventWithRole: components["schemas"]["Event"] & { + event_role?: { + event_role_type: components["schemas"]["EventRole"]; + valid: boolean; + }; + }; }; responses: { /** @description Unauthenticated: Requester is not currently authenticated. */ @@ -464,7 +470,10 @@ export interface operations { }; "get-events": { parameters: { - query?: never; + query?: { + /** @description If true, include unpublished events as well. Superusers ONLY. */ + include_published?: boolean; + }; header?: never; path?: never; cookie?: never; @@ -477,7 +486,7 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["Event"][]; + "application/json": components["schemas"]["EventWithRole"][]; }; }; }; From bd05d14d688317bd0eadb5cadf0e6eb22f34f124 Mon Sep 17 00:00:00 2001 From: Alexander Wang Date: Mon, 18 Aug 2025 23:34:28 -0400 Subject: [PATCH 2/3] feat: Event card integration --- apps/api/internal/api/handlers/events.go | 2 +- apps/api/internal/ctxutils/user.go | 1 + apps/api/internal/db/queries/events.sql | 8 +- apps/api/internal/db/repository/events.go | 4 +- apps/api/internal/db/sqlc/events.sql.go | 56 +- apps/api/internal/db/sqlc/querier.go | 2 +- apps/api/internal/services/events.go | 2 +- .../EventManager/hooks/useAdminEvents.ts | 2 +- .../features/Event/components/EventCard.tsx | 2 +- .../Event/hooks/useEventsWithUserInfo.ts | 21 + apps/web/src/features/Event/utils/mapper.ts | 42 + apps/web/src/lib/openapi/schema.d.ts | 1489 +++++++++-------- apps/web/src/lib/openapi/types.ts | 1 + apps/web/src/routes/_main/portal.tsx | 44 +- shared/openapi/core-api.yaml | 50 +- 15 files changed, 944 insertions(+), 782 deletions(-) create mode 100644 apps/web/src/features/Event/hooks/useEventsWithUserInfo.ts create mode 100644 apps/web/src/features/Event/utils/mapper.ts diff --git a/apps/api/internal/api/handlers/events.go b/apps/api/internal/api/handlers/events.go index 285bd24c..4ed16c17 100644 --- a/apps/api/internal/api/handlers/events.go +++ b/apps/api/internal/api/handlers/events.go @@ -224,7 +224,7 @@ func (h *EventHandler) DeleteEventById(w http.ResponseWriter, r *http.Request) { func (h *EventHandler) GetEvents(w http.ResponseWriter, r *http.Request) { // Parse query params - // inlcude_unpublished="true,false", default: false + // include_unpublished="true,false", default: false queryParams := r.URL.Query() includeUnpublished, err := web.ParseParamBoolean(queryParams, "include_unpublished", ptr.BoolToPtr(false)) if err != nil { diff --git a/apps/api/internal/ctxutils/user.go b/apps/api/internal/ctxutils/user.go index a04ff8c3..d5e86ca8 100644 --- a/apps/api/internal/ctxutils/user.go +++ b/apps/api/internal/ctxutils/user.go @@ -28,6 +28,7 @@ func IsUser(ctx context.Context) bool { return userCtx.Role == sqlc.AuthUserRoleUser } +// Takes in the request context and returns the userID or nil if not retrievable func GetUserIdFromCtx(ctx context.Context) *uuid.UUID { userCtx, ok := ctx.Value(mw.UserContextKey).(*mw.UserContext) if !ok { diff --git a/apps/api/internal/db/queries/events.sql b/apps/api/internal/db/queries/events.sql index d25965e4..5cd95330 100644 --- a/apps/api/internal/db/queries/events.sql +++ b/apps/api/internal/db/queries/events.sql @@ -76,13 +76,17 @@ LEFT JOIN event_roles AS er AND er.user_id = $1 ORDER BY e.start_time ASC; --- name: GetEventsWithRoles :many +-- name: GetEventsWithUserInfo :many SELECT e.*, - er.role AS event_role + er.role AS event_role, + a.status AS application_status FROM events e LEFT JOIN event_roles er ON er.event_id = e.id AND er.user_id = sqlc.narg(user_id) +LEFT JOIN applications a + ON a.event_id = e.id + AND a.user_id = sqlc.narg(user_id) WHERE (sqlc.arg(include_unpublished)::boolean IS TRUE OR e.is_published = TRUE) ORDER BY e.start_time ASC; diff --git a/apps/api/internal/db/repository/events.go b/apps/api/internal/db/repository/events.go index 3da63069..cb544035 100644 --- a/apps/api/internal/db/repository/events.go +++ b/apps/api/internal/db/repository/events.go @@ -84,8 +84,8 @@ func (r *EventRepository) GetPublishedEvents(ctx context.Context, userId uuid.UU return &events, err } -func (r *EventRepository) GetEventsWithRoles(ctx context.Context, userId *uuid.UUID, includeUnpublished bool) (*[]sqlc.GetEventsWithRolesRow, error) { - events, err := r.db.Query.GetEventsWithRoles(ctx, sqlc.GetEventsWithRolesParams{ +func (r *EventRepository) GetEventsWithRoles(ctx context.Context, userId *uuid.UUID, includeUnpublished bool) (*[]sqlc.GetEventsWithUserInfoRow, error) { + events, err := r.db.Query.GetEventsWithUserInfo(ctx, sqlc.GetEventsWithUserInfoParams{ UserID: userId, IncludeUnpublished: includeUnpublished, }) diff --git a/apps/api/internal/db/sqlc/events.sql.go b/apps/api/internal/db/sqlc/events.sql.go index d2b39012..dc33caf0 100644 --- a/apps/api/internal/db/sqlc/events.sql.go +++ b/apps/api/internal/db/sqlc/events.sql.go @@ -225,52 +225,57 @@ func (q *Queries) GetEventRoleByIds(ctx context.Context, arg GetEventRoleByIdsPa return i, err } -const getEventsWithRoles = `-- name: GetEventsWithRoles :many +const getEventsWithUserInfo = `-- name: GetEventsWithUserInfo :many SELECT e.id, e.name, e.description, e.location, e.location_url, e.max_attendees, e.application_open, e.application_close, e.rsvp_deadline, e.decision_release, e.start_time, e.end_time, e.website_url, e.is_published, e.created_at, e.updated_at, - er.role AS event_role + er.role AS event_role, + a.status AS application_status FROM events e LEFT JOIN event_roles er ON er.event_id = e.id AND er.user_id = $1 +LEFT JOIN applications a + ON a.event_id = e.id + AND a.user_id = $1 WHERE ($2::boolean IS TRUE OR e.is_published = TRUE) ORDER BY e.start_time ASC ` -type GetEventsWithRolesParams struct { +type GetEventsWithUserInfoParams struct { UserID *uuid.UUID `json:"user_id"` IncludeUnpublished bool `json:"include_unpublished"` } -type GetEventsWithRolesRow struct { - ID uuid.UUID `json:"id"` - Name string `json:"name"` - Description *string `json:"description"` - Location *string `json:"location"` - LocationUrl *string `json:"location_url"` - MaxAttendees *int32 `json:"max_attendees"` - ApplicationOpen time.Time `json:"application_open"` - ApplicationClose time.Time `json:"application_close"` - RsvpDeadline *time.Time `json:"rsvp_deadline"` - DecisionRelease *time.Time `json:"decision_release"` - StartTime time.Time `json:"start_time"` - EndTime time.Time `json:"end_time"` - WebsiteUrl *string `json:"website_url"` - IsPublished *bool `json:"is_published"` - CreatedAt *time.Time `json:"created_at"` - UpdatedAt *time.Time `json:"updated_at"` - EventRole NullEventRoleType `json:"event_role"` +type GetEventsWithUserInfoRow struct { + ID uuid.UUID `json:"id"` + Name string `json:"name"` + Description *string `json:"description"` + Location *string `json:"location"` + LocationUrl *string `json:"location_url"` + MaxAttendees *int32 `json:"max_attendees"` + ApplicationOpen time.Time `json:"application_open"` + ApplicationClose time.Time `json:"application_close"` + RsvpDeadline *time.Time `json:"rsvp_deadline"` + DecisionRelease *time.Time `json:"decision_release"` + StartTime time.Time `json:"start_time"` + EndTime time.Time `json:"end_time"` + WebsiteUrl *string `json:"website_url"` + IsPublished *bool `json:"is_published"` + CreatedAt *time.Time `json:"created_at"` + UpdatedAt *time.Time `json:"updated_at"` + EventRole NullEventRoleType `json:"event_role"` + ApplicationStatus NullApplicationStatus `json:"application_status"` } -func (q *Queries) GetEventsWithRoles(ctx context.Context, arg GetEventsWithRolesParams) ([]GetEventsWithRolesRow, error) { - rows, err := q.db.Query(ctx, getEventsWithRoles, arg.UserID, arg.IncludeUnpublished) +func (q *Queries) GetEventsWithUserInfo(ctx context.Context, arg GetEventsWithUserInfoParams) ([]GetEventsWithUserInfoRow, error) { + rows, err := q.db.Query(ctx, getEventsWithUserInfo, arg.UserID, arg.IncludeUnpublished) if err != nil { return nil, err } defer rows.Close() - items := []GetEventsWithRolesRow{} + items := []GetEventsWithUserInfoRow{} for rows.Next() { - var i GetEventsWithRolesRow + var i GetEventsWithUserInfoRow if err := rows.Scan( &i.ID, &i.Name, @@ -289,6 +294,7 @@ func (q *Queries) GetEventsWithRoles(ctx context.Context, arg GetEventsWithRoles &i.CreatedAt, &i.UpdatedAt, &i.EventRole, + &i.ApplicationStatus, ); err != nil { return nil, err } diff --git a/apps/api/internal/db/sqlc/querier.go b/apps/api/internal/db/sqlc/querier.go index 3027771c..e7806f69 100644 --- a/apps/api/internal/db/sqlc/querier.go +++ b/apps/api/internal/db/sqlc/querier.go @@ -35,7 +35,7 @@ type Querier interface { GetEventByID(ctx context.Context, id uuid.UUID) (Event, error) GetEventRoleByIds(ctx context.Context, arg GetEventRoleByIdsParams) (EventRole, error) GetEventStaff(ctx context.Context, eventID uuid.UUID) ([]GetEventStaffRow, error) - GetEventsWithRoles(ctx context.Context, arg GetEventsWithRolesParams) ([]GetEventsWithRolesRow, error) + GetEventsWithUserInfo(ctx context.Context, arg GetEventsWithUserInfoParams) ([]GetEventsWithUserInfoRow, error) GetPublishedEvents(ctx context.Context, userID uuid.UUID) ([]GetPublishedEventsRow, error) GetSessionByID(ctx context.Context, id uuid.UUID) (AuthSession, error) GetSessionsByUserID(ctx context.Context, userID uuid.UUID) ([]AuthSession, error) diff --git a/apps/api/internal/services/events.go b/apps/api/internal/services/events.go index 3c4a56eb..f174329d 100644 --- a/apps/api/internal/services/events.go +++ b/apps/api/internal/services/events.go @@ -98,7 +98,7 @@ func (s *EventService) DeleteEventById(ctx context.Context, id uuid.UUID) error return err } -func (s *EventService) GetEvents(ctx context.Context, includeUnpublished bool) (*[]sqlc.GetEventsWithRolesRow, error) { +func (s *EventService) GetEvents(ctx context.Context, includeUnpublished bool) (*[]sqlc.GetEventsWithUserInfoRow, error) { isSuperuser := ctxu.IsSuperuser(ctx) userId := ctxu.GetUserIdFromCtx(ctx) diff --git a/apps/web/src/features/Admin/EventManager/hooks/useAdminEvents.ts b/apps/web/src/features/Admin/EventManager/hooks/useAdminEvents.ts index cacf8baf..14e0e9f2 100644 --- a/apps/web/src/features/Admin/EventManager/hooks/useAdminEvents.ts +++ b/apps/web/src/features/Admin/EventManager/hooks/useAdminEvents.ts @@ -8,7 +8,7 @@ type Events = operations["get-events"]["responses"]["200"]["content"]["application/json"]; export async function fetchEvents(): Promise { - const result = await api.get("events").json(); + const result = await api.get("events?include_unpublished=true").json(); console.log(result); return result; } diff --git a/apps/web/src/features/Event/components/EventCard.tsx b/apps/web/src/features/Event/components/EventCard.tsx index 002dc47d..b3b46a68 100644 --- a/apps/web/src/features/Event/components/EventCard.tsx +++ b/apps/web/src/features/Event/components/EventCard.tsx @@ -10,7 +10,7 @@ import type applicationStatus from "../applicationStatus"; import { Separator } from "@/components/ui/Seperator"; import { Card } from "@/components/ui/Card"; -interface EventCardProps { +export interface EventCardProps { eventId: string; status: keyof typeof applicationStatus; title: string; diff --git a/apps/web/src/features/Event/hooks/useEventsWithUserInfo.ts b/apps/web/src/features/Event/hooks/useEventsWithUserInfo.ts new file mode 100644 index 00000000..1a79b460 --- /dev/null +++ b/apps/web/src/features/Event/hooks/useEventsWithUserInfo.ts @@ -0,0 +1,21 @@ +import { api } from "@/lib/ky"; +import type { operations } from "@/lib/openapi/schema"; +import { useQuery } from "@tanstack/react-query"; + +export const eventsQueryKey = ["events", "published"] as const; + +export type EventsWithUserInfo = + operations["get-events"]["responses"]["200"]["content"]["application/json"]; + +export async function fetchEvents(): Promise { + const result = await api.get("events?include_unpublished=false").json(); + return result; +} + +export function useAdminEvents() { + return useQuery({ + queryKey: eventsQueryKey, + queryFn: fetchEvents, + staleTime: 1000 * 60 * 5, // 5 minutes + }); +} diff --git a/apps/web/src/features/Event/utils/mapper.ts b/apps/web/src/features/Event/utils/mapper.ts new file mode 100644 index 00000000..d3851dd4 --- /dev/null +++ b/apps/web/src/features/Event/utils/mapper.ts @@ -0,0 +1,42 @@ +// Mapping event api reponse to usable structure for event card + +import type { EventWithUserInfo } from "@/lib/openapi/types"; +import type applicationStatus from "../applicationStatus"; +import type { EventCardProps } from "../components/EventCard"; +import { format } from "date-fns"; + +function formatDateRange(start: Date, end: Date): string { + const startDay = format(start, "d"); + const endDay = format(end, "do"); // adds "st", "nd", "rd", "th" + const month = format(start, "MMM"); + + // If start and end are in the same month + if (format(start, "MMM") === format(end, "MMM")) { + return `${month} ${startDay}-${endDay}`; + } + + // Different months + const startMonth = format(start, "MMM"); + const endMonth = format(end, "MMM"); + return `${startMonth} ${startDay} - ${endMonth} ${endDay}`; +} + +function mapEventsAPIResponseToEventCardProps(data: EventWithUserInfo): EventCardProps { + let status: keyof typeof applicationStatus = "notApplied" // Default + + if (!data.application_status) { + return { + eventId: data.id, + status, + title: data.name, + description: data.description ?? "No description", + date: + } + } + + switch (data.application_status?.application_status) { + case "under_review" && data.application_status?.valid == true: + + } +} + diff --git a/apps/web/src/lib/openapi/schema.d.ts b/apps/web/src/lib/openapi/schema.d.ts index a81cf438..7025ee2c 100644 --- a/apps/web/src/lib/openapi/schema.d.ts +++ b/apps/web/src/lib/openapi/schema.d.ts @@ -4,754 +4,763 @@ */ export interface paths { - "/auth/callback": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; + "/auth/callback": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * OAuth2 Auth Callback + * @description This route is used for OAuth authentication methods to verify and login/create an account. + */ + post: operations["post-v1-auth-callback"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/auth/me": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Current User + * @description Get the currently authenticated user's information. + */ + get: operations["get-auth-me"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/events/{eventId}/interest": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Make an interest submission for an event (email list) */ + post: operations["post-event-interest"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/events": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get events + * @description Gets events with a nullable event role for authenticated users. + */ + get: operations["get-events"]; + put?: never; + /** Create a new event */ + post: operations["post-event"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/events/{eventId}": { + parameters: { + query?: never; + header?: never; + path: { + eventId: string; + }; + cookie?: never; + }; + /** Get an event */ + get: operations["get-single-event"]; + put?: never; + post?: never; + /** Delete an event */ + delete: operations["delete-event"]; + options?: never; + head?: never; + /** Update an event */ + patch: operations["patch-event"]; + trace?: never; + }; + "/events/{eventId}/staff": { + parameters: { + query?: never; + header?: never; + path: { + eventId: string; + }; + cookie?: never; + }; + /** + * Get all staff users for an event + * @description Gets all users with role STAFF or ADMIN + */ + get: operations["get-event-staff"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/events/{eventId}/roles": { + parameters: { + query?: never; + header?: never; + path: { + eventId: string; + }; + cookie?: never; + }; + get?: never; + put?: never; + /** Change or add event role of a user */ + post: operations["post-event-role"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; - get?: never; - put?: never; - /** - * OAuth2 Auth Callback - * @description This route is used for OAuth authentication methods to verify and login/create an account. - */ - post: operations["post-v1-auth-callback"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/auth/me": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get Current User - * @description Get the currently authenticated user's information. - */ - get: operations["get-auth-me"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/events/{eventId}/interest": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** Make an interest submission for an event (email list) */ - post: operations["post-event-interest"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/events": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get events - * @description Gets events with a nullable event role for authenticated users. - */ - get: operations["get-events"]; - put?: never; - /** Create a new event */ - post: operations["post-event"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/events/{eventId}": { - parameters: { - query?: never; - header?: never; - path: { - eventId: string; - }; - cookie?: never; - }; - /** Get an event */ - get: operations["get-single-event"]; - put?: never; - post?: never; - /** Delete an event */ - delete: operations["delete-event"]; - options?: never; - head?: never; - /** Update an event */ - patch: operations["patch-event"]; - trace?: never; - }; - "/events/{eventId}/staff": { - parameters: { - query?: never; - header?: never; - path: { - eventId: string; - }; - cookie?: never; - }; - /** - * Get all staff users for an event - * @description Gets all users with role STAFF or ADMIN - */ - get: operations["get-event-staff"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/events/{eventId}/roles": { - parameters: { - query?: never; - header?: never; - path: { - eventId: string; - }; - cookie?: never; - }; - get?: never; - put?: never; - /** Change or add event role of a user */ - post: operations["post-event-role"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; } export type webhooks = Record; export interface components { - schemas: { - Event: { - /** Format: uuid */ - id: string; - name: string; - description?: string | null; - location?: string | null; - /** Format: uri */ - location_url?: string | null; - /** Format: int32 */ - max_attendees?: number | null; - /** Format: date-time */ - application_open: string; - /** Format: date-time */ - application_close: string; - /** Format: date-time */ - rsvp_deadline?: string | null; - /** Format: date-time */ - decision_release?: string | null; - /** Format: date-time */ - start_time: string; - /** Format: date-time */ - end_time: string; - /** Format: uri */ - website_url?: string | null; - is_published?: boolean | null; - /** Format: date-time */ - created_at?: string | null; - /** Format: date-time */ - updated_at?: string | null; - }; - /** - * ErrorResponse - * @description This model is returned on server errors, it returns an error code (lookup code definitions in documentation), an error key, and a message. - */ - ErrorResponse: { - error: string; - message: string; - }; - /** - * UserContext - * @description This is the model used when returning from GetMe. Used often in middleware! - */ - UserContext: { - /** Format: uuid */ - userId: string; - name: string; - onboarded: boolean; - /** Format: uri */ - image?: string | null; - role: components["schemas"]["PlatformRole"]; - }; - /** - * PlatformRole - * @description A user's role on the platform. Either base permissions or elevated superuser perms. - * @enum {string} - */ - PlatformRole: "user" | "superuser"; - /** Session */ - Session: { - /** Format: uuid */ - id: string; - /** Format: uuid */ - user_id: string; - /** Format: date-time */ - expires_at: string; - ip_address?: string | null; - user_agent?: string | null; - }; - User: { - /** - * Format: uuid - * @example 123e4567-e89b-12d3-a456-426614174000 - */ - id: string; - /** @example John Doe */ - name: string; - /** - * Format: email - * @example john@example.com - */ - email?: string | null; - /** @example true */ - email_verified: boolean; - /** @example false */ - onboarded: boolean; - /** - * Format: uri - * @example https://example.com/avatar.jpg - */ - image?: string | null; - /** - * Format: date-time - * @example 2025-08-08T17:00:00Z - */ - created_at: string; - /** - * Format: date-time - * @example 2025-08-08T18:00:00Z - */ - updated_at: string; - role: components["schemas"]["UserRole"]; - }; - UserWithEventRole: components["schemas"]["User"] & { - event_role: components["schemas"]["EventRole"]; - }; - /** - * @example user - * @enum {string} - */ - UserRole: "user" | "superuser"; - /** - * @example attendee - * @enum {string} - */ - EventRole: "admin" | "staff" | "attendee" | "applicant"; - EventWithRole: components["schemas"]["Event"] & { - event_role?: { - event_role_type: components["schemas"]["EventRole"]; - valid: boolean; - }; - }; - }; - responses: { - /** @description Unauthenticated: Requester is not currently authenticated. */ - Unauthenticated: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ErrorResponse"]; - }; - }; - /** @description Unauthorized: User does not have access to this resource. */ - Unauthorized: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ErrorResponse"]; - }; - }; - "Bad-Request": { - headers: { - [name: string]: unknown; - }; - content?: never; + schemas: { + Event: { + /** Format: uuid */ + id: string; + name: string; + description?: string | null; + location?: string | null; + /** Format: uri */ + location_url?: string | null; + /** Format: int32 */ + max_attendees?: number | null; + /** Format: date-time */ + application_open: string; + /** Format: date-time */ + application_close: string; + /** Format: date-time */ + rsvp_deadline?: string | null; + /** Format: date-time */ + decision_release?: string | null; + /** Format: date-time */ + start_time: string; + /** Format: date-time */ + end_time: string; + /** Format: uri */ + website_url?: string | null; + is_published?: boolean | null; + /** Format: date-time */ + created_at?: string | null; + /** Format: date-time */ + updated_at?: string | null; + }; + /** + * ErrorResponse + * @description This model is returned on server errors, it returns an error code (lookup code definitions in documentation), an error key, and a message. + */ + ErrorResponse: { + error: string; + message: string; + }; + /** + * UserContext + * @description This is the model used when returning from GetMe. Used often in middleware! + */ + UserContext: { + /** Format: uuid */ + userId: string; + name: string; + onboarded: boolean; + /** Format: uri */ + image?: string | null; + role: components["schemas"]["PlatformRole"]; + }; + /** + * PlatformRole + * @description A user's role on the platform. Either base permissions or elevated superuser perms. + * @enum {string} + */ + PlatformRole: "user" | "superuser"; + /** Session */ + Session: { + /** Format: uuid */ + id: string; + /** Format: uuid */ + user_id: string; + /** Format: date-time */ + expires_at: string; + ip_address?: string | null; + user_agent?: string | null; + }; + User: { + /** + * Format: uuid + * @example 123e4567-e89b-12d3-a456-426614174000 + */ + id: string; + /** @example John Doe */ + name: string; + /** + * Format: email + * @example john@example.com + */ + email?: string | null; + /** @example true */ + email_verified: boolean; + /** @example false */ + onboarded: boolean; + /** + * Format: uri + * @example https://example.com/avatar.jpg + */ + image?: string | null; + /** + * Format: date-time + * @example 2025-08-08T17:00:00Z + */ + created_at: string; + /** + * Format: date-time + * @example 2025-08-08T18:00:00Z + */ + updated_at: string; + role: components["schemas"]["UserRole"]; + }; + UserWithEventRole: components["schemas"]["User"] & { + event_role: components["schemas"]["EventRole"]; + }; + /** + * @example user + * @enum {string} + */ + UserRole: "user" | "superuser"; + /** + * @example attendee + * @enum {string} + */ + EventRole: "admin" | "staff" | "attendee" | "applicant"; + /** + * @example under_review + * @enum {string} + */ + ApplicationStatus: "started" | "submitted" | "under_review" | "accepted" | "rejected" | "waitlisted" | "withdrawn"; + EventWithUserInfo: components["schemas"]["Event"] & { + event_role?: { + event_role_type: components["schemas"]["EventRole"]; + valid: boolean; + }; + application_status?: { + application_status: components["schemas"]["ApplicationStatus"]; + valid: boolean; + }; + }; }; - }; - parameters: never; - requestBodies: never; - headers: never; - pathItems: never; + responses: { + /** @description Unauthenticated: Requester is not currently authenticated. */ + Unauthenticated: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Unauthorized: User does not have access to this resource. */ + Unauthorized: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + "Bad-Request": { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; } export type $defs = Record; export interface operations { - "post-v1-auth-callback": { - parameters: { - query: { - /** @description The OAuth code passed back from the provider. Part of the PKCE flow. */ - code: string; - /** @description The state containing a base64 encoded version of the nonce, provider, and redirect url. */ - state: string; - }; - header?: never; - path?: never; - cookie: { - /** @description The nonce for comparing against the callback state decoded to prevent CSRF attacks. */ - sh_auth_nonce: string; - }; - }; - requestBody?: never; - responses: { - /** @description OK: User is logged in successfully. For when the redirect field is empty. */ - 200: { - headers: { - /** @description Sets a sh_session cookie to signify auth status */ - "Set-Cookie"?: string; - [name: string]: unknown; - }; - content?: never; - }; - /** @description Found: Logged in and redirected to a requested location */ - 302: { - headers: { - /** @description Sets a sh_session cookie to signify auth status */ - "Set-Cookie"?: string; - [name: string]: unknown; - }; - content?: never; - }; - /** @description Bad Request: Something went wrong with the request queries or their properties */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ErrorResponse"]; - }; - }; - /** @description Forbidden: Something went wrong verifying identity or authenticating. */ - 403: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ErrorResponse"]; - }; - }; - /** @description Internal Server Error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ErrorResponse"]; - }; - }; - /** @description Bad Gateway: Authenticating OAuth server did not respond or user does not exist */ - 502: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ErrorResponse"]; - }; - }; - }; - }; - "get-auth-me": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: { - /** @description The authenticated session token/id */ - sh_session?: string; - }; - }; - requestBody?: never; - responses: { - /** @description OK: Current user data returned */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["UserContext"]; - }; - }; - 401: components["responses"]["Unauthenticated"]; - /** @description Server Error: Something went terribly wrong on our end. */ - "5XX": { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ErrorResponse"]; - }; - }; - }; - }; - "post-event-interest": { - parameters: { - query?: never; - header?: never; - path: { - eventId: string; - }; - cookie?: never; - }; - requestBody: { - content: { - "application/json": { - /** - * Format: email - * @example johndoe@ufl.edu - */ - email: string; - /** @example SHX Frontpage */ - source?: string; - }; - }; - }; - responses: { - /** @description OK: Interest email created */ - 201: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Bad request/Malformed request */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ErrorResponse"]; - }; - }; - /** @description Duplicate email found in DB */ - 409: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ErrorResponse"]; - }; - }; - /** @description Server Error: Something went terribly wrong on our end. */ - "5XX": { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ErrorResponse"]; - }; - }; - }; - }; - "get-events": { - parameters: { - query?: { - /** @description If true, include unpublished events as well. Superusers ONLY. */ - include_published?: boolean; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description OK: Events returned */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["EventWithRole"][]; - }; - }; - }; - }; - "post-event": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": { - /** @example Open Software Club's Workshop */ - name: string; - /** - * Format: date-time - * @example 2025-08-01T08:00:00Z - */ - application_open: string; - /** - * Format: date-time - * @example 2025-12-01T023:59:00Z - */ - application_close: string; - /** - * Format: date-time - * @example 2026-02-10T17:00:00Z - */ - start_time: string; - /** - * Format: date-time - * @example 2026-02-12T08:00:00Z - */ - end_time: string; - description?: string; - location?: string; - location_url?: string; - max_attendees?: number; - /** - * Format: date-time - * @example 2025-08-01T08:00:00Z - */ - rsvp_deadline?: string; - /** - * Format: date-time - * @example 2025-08-01T08:00:00Z - */ - decision_release?: string; - website_url?: string; - is_published?: boolean; - }; - }; - }; - responses: { - /** @description OK: Event created */ - 201: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": unknown; - }; - }; - /** @description Bad request/Malformed request */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": unknown; - examples: unknown; - }; - }; - /** @description endTime is before startTime or applicationClose is before applicationOpen */ - 409: { - headers: { - [name: string]: unknown; - }; - content: { - schema: unknown; - examples: unknown; - }; - }; - /** @description Server Error: Something went terribly wrong on our end. */ - "5XX": { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ErrorResponse"]; - }; - }; - }; - }; - "get-single-event": { - parameters: { - query?: never; - header?: never; - path: { - eventId: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description OK - Event received */ - 201: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["Event"]; + "post-v1-auth-callback": { + parameters: { + query: { + /** @description The OAuth code passed back from the provider. Part of the PKCE flow. */ + code: string; + /** @description The state containing a base64 encoded version of the nonce, provider, and redirect url. */ + state: string; + }; + header?: never; + path?: never; + cookie: { + /** @description The nonce for comparing against the callback state decoded to prevent CSRF attacks. */ + sh_auth_nonce: string; + }; + }; + requestBody?: never; + responses: { + /** @description OK: User is logged in successfully. For when the redirect field is empty. */ + 200: { + headers: { + /** @description Sets a sh_session cookie to signify auth status */ + "Set-Cookie"?: string; + [name: string]: unknown; + }; + content?: never; + }; + /** @description Found: Logged in and redirected to a requested location */ + 302: { + headers: { + /** @description Sets a sh_session cookie to signify auth status */ + "Set-Cookie"?: string; + [name: string]: unknown; + }; + content?: never; + }; + /** @description Bad Request: Something went wrong with the request queries or their properties */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Forbidden: Something went wrong verifying identity or authenticating. */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Bad Gateway: Authenticating OAuth server did not respond or user does not exist */ + 502: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + "get-auth-me": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: { + /** @description The authenticated session token/id */ + sh_session?: string; + }; + }; + requestBody?: never; + responses: { + /** @description OK: Current user data returned */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UserContext"]; + }; + }; + 401: components["responses"]["Unauthenticated"]; + /** @description Server Error: Something went terribly wrong on our end. */ + "5XX": { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + "post-event-interest": { + parameters: { + query?: never; + header?: never; + path: { + eventId: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** + * Format: email + * @example johndoe@ufl.edu + */ + email: string; + /** @example SHX Frontpage */ + source?: string; + }; + }; + }; + responses: { + /** @description OK: Interest email created */ + 201: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Bad request/Malformed request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Duplicate email found in DB */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Server Error: Something went terribly wrong on our end. */ + "5XX": { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + "get-events": { + parameters: { + query?: { + /** @description If true, include unpublished events as well. Superusers ONLY. */ + include_published?: boolean; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK: Events returned */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["EventWithUserInfo"][]; + }; + }; + }; + }; + "post-event": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** @example Open Software Club's Workshop */ + name: string; + /** + * Format: date-time + * @example 2025-08-01T08:00:00Z + */ + application_open: string; + /** + * Format: date-time + * @example 2025-12-01T023:59:00Z + */ + application_close: string; + /** + * Format: date-time + * @example 2026-02-10T17:00:00Z + */ + start_time: string; + /** + * Format: date-time + * @example 2026-02-12T08:00:00Z + */ + end_time: string; + description?: string; + location?: string; + location_url?: string; + max_attendees?: number; + /** + * Format: date-time + * @example 2025-08-01T08:00:00Z + */ + rsvp_deadline?: string; + /** + * Format: date-time + * @example 2025-08-01T08:00:00Z + */ + decision_release?: string; + website_url?: string; + is_published?: boolean; + }; + }; + }; + responses: { + /** @description OK: Event created */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Bad request/Malformed request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + examples: unknown; + }; + }; + /** @description endTime is before startTime or applicationClose is before applicationOpen */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + schema: unknown; + examples: unknown; + }; + }; + /** @description Server Error: Something went terribly wrong on our end. */ + "5XX": { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + "get-single-event": { + parameters: { + query?: never; + header?: never; + path: { + eventId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK - Event received */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Event"]; + }; + }; + /** @description Server Error: Something went terribly wrong on our end. */ + "5XX": { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + "delete-event": { + parameters: { + query?: never; + header?: never; + path: { + eventId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK - Event deleted (patched) */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Server Error: Something went terribly wrong on our end. */ + "5XX": { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + "patch-event": { + parameters: { + query?: never; + header?: never; + path: { + eventId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK - Event updated (patched) */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Server Error: Something went terribly wrong on our end. */ + "5XX": { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + "get-event-staff": { + parameters: { + query?: never; + header?: never; + path: { + eventId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK - Return users */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UserWithEventRole"][]; + }; + }; + /** @description Server Error: Something went terribly wrong on our end. */ + "5XX": { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + "post-event-role": { + parameters: { + query?: never; + header?: never; + path: { + eventId: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** @example user@example.com */ + email?: string | null; + /** + * Format: uuid + * @example 123e4567-e89b-12d3-a456-426614174000 + */ + user_id?: string | null; + role: components["schemas"]["EventRole"]; + }; + }; + }; + responses: { + /** @description OK - Return users */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Not Found - User not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Server Error: Something went terribly wrong on our end. */ + "5XX": { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; }; - }; - /** @description Server Error: Something went terribly wrong on our end. */ - "5XX": { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ErrorResponse"]; - }; - }; - }; - }; - "delete-event": { - parameters: { - query?: never; - header?: never; - path: { - eventId: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description OK - Event deleted (patched) */ - 204: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Server Error: Something went terribly wrong on our end. */ - "5XX": { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ErrorResponse"]; - }; - }; - }; - }; - "patch-event": { - parameters: { - query?: never; - header?: never; - path: { - eventId: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description OK - Event updated (patched) */ - 204: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Server Error: Something went terribly wrong on our end. */ - "5XX": { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ErrorResponse"]; - }; - }; - }; - }; - "get-event-staff": { - parameters: { - query?: never; - header?: never; - path: { - eventId: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description OK - Return users */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["UserWithEventRole"][]; - }; - }; - /** @description Server Error: Something went terribly wrong on our end. */ - "5XX": { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ErrorResponse"]; - }; - }; - }; - }; - "post-event-role": { - parameters: { - query?: never; - header?: never; - path: { - eventId: string; - }; - cookie?: never; - }; - requestBody: { - content: { - "application/json": { - /** @example user@example.com */ - email?: string | null; - /** - * Format: uuid - * @example 123e4567-e89b-12d3-a456-426614174000 - */ - user_id?: string | null; - role: components["schemas"]["EventRole"]; - }; - }; - }; - responses: { - /** @description OK - Return users */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Not Found - User not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ErrorResponse"]; - }; - }; - /** @description Server Error: Something went terribly wrong on our end. */ - "5XX": { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ErrorResponse"]; - }; - }; }; - }; } diff --git a/apps/web/src/lib/openapi/types.ts b/apps/web/src/lib/openapi/types.ts index 5a508362..d3c10b50 100644 --- a/apps/web/src/lib/openapi/types.ts +++ b/apps/web/src/lib/openapi/types.ts @@ -8,3 +8,4 @@ export type Event = components["schemas"]["Event"]; export type CreateEvent = operations["post-event"]["requestBody"]["content"]["application/json"]; export type User = components["schemas"]["User"]; +export type EventWithUserInfo = components["schemas"]["EventWithUserInfo"] diff --git a/apps/web/src/routes/_main/portal.tsx b/apps/web/src/routes/_main/portal.tsx index 6e7e9725..e3926c49 100644 --- a/apps/web/src/routes/_main/portal.tsx +++ b/apps/web/src/routes/_main/portal.tsx @@ -1,12 +1,15 @@ import { Button } from "@/components/ui/Button"; +import { EventCard } from "@/features/Event/components/EventCard"; import { auth } from "@/lib/authClient"; import { createFileRoute } from "@tanstack/react-router"; +import { Heading } from "react-aria-components"; export const Route = createFileRoute("/_main/portal")({ component: RouteComponent, }); function RouteComponent() { + const { user } = Route.useRouteContext(); const logout = async () => { try { await auth.logOut(); @@ -17,11 +20,42 @@ function RouteComponent() { }; return ( -
-

Event Portal

- +
+
+ + Welcome, {user?.name ?? "hacker"}! + +

Ready to start hacking?

+
+ +
+ + + + + +
); } diff --git a/shared/openapi/core-api.yaml b/shared/openapi/core-api.yaml index 70600b5f..8ae1dd1f 100644 --- a/shared/openapi/core-api.yaml +++ b/shared/openapi/core-api.yaml @@ -233,8 +233,16 @@ paths: - Event /events: get: - summary: Get all events - description: Gets only published events for normal users, and all events for superusers + summary: Get events + description: Gets events with a nullable event role for authenticated users. + parameters: + - name: include_published + in: query + description: If true, include unpublished events as well. Superusers ONLY. + required: false + schema: + type: boolean + default: false responses: '200': description: 'OK: Events returned' @@ -243,7 +251,7 @@ paths: schema: type: array items: - $ref: '#/components/schemas/Event' + $ref: '#/components/schemas/EventWithUserInfo' operationId: get-events tags: @@ -703,6 +711,42 @@ components: - attendee - applicant example: attendee + ApplicationStatus: + type: string + enum: + - started + - submitted + - under_review + - accepted + - rejected + - waitlisted + - withdrawn + example: under_review + EventWithUserInfo: + allOf: + - $ref: '#/components/schemas/Event' + - type: object + properties: + event_role: + type: object + properties: + event_role_type: + $ref: '#/components/schemas/EventRole' + valid: + type: boolean + required: + - event_role_type + - valid + application_status: + type: object + properties: + application_status: + $ref: '#/components/schemas/ApplicationStatus' + valid: + type: boolean + required: + - application_status + - valid securitySchemes: sh_session_id: type: http From bfab08c32df14883d4ec73265f053eb0c65bc6f0 Mon Sep 17 00:00:00 2001 From: AlexanderWangY Date: Tue, 19 Aug 2025 00:32:31 -0400 Subject: [PATCH 3/3] feat: click action on event button --- apps/api/go.sum | 2 - apps/web/package.json | 1 + apps/web/pnpm-lock.yaml | 8 + .../EventManager/hooks/useAdminEvents.ts | 4 +- .../features/Event/components/EventButton.tsx | 56 + .../features/Event/components/EventCard.tsx | 13 +- .../Event/hooks/useEventsWithUserInfo.ts | 10 +- apps/web/src/features/Event/utils/mapper.ts | 79 +- apps/web/src/lib/openapi/schema.d.ts | 1505 +++++++++-------- apps/web/src/lib/openapi/types.ts | 2 +- apps/web/src/routes/_main/portal.tsx | 94 +- .../events/$eventId/dashboard/index.tsx | 9 + .../events/$eventId/feedback/declined.tsx | 9 + .../src/routes/events/$eventId/rejected.tsx | 9 + .../src/routes/events/$eventId/summary.tsx | 9 + .../routes/events/$eventId/waitlist/info.tsx | 9 + 16 files changed, 1009 insertions(+), 810 deletions(-) create mode 100644 apps/web/src/routes/events/$eventId/dashboard/index.tsx create mode 100644 apps/web/src/routes/events/$eventId/feedback/declined.tsx create mode 100644 apps/web/src/routes/events/$eventId/rejected.tsx create mode 100644 apps/web/src/routes/events/$eventId/summary.tsx create mode 100644 apps/web/src/routes/events/$eventId/waitlist/info.tsx diff --git a/apps/api/go.sum b/apps/api/go.sum index 75898a9e..08bbbd99 100644 --- a/apps/api/go.sum +++ b/apps/api/go.sum @@ -56,8 +56,6 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/oapi-codegen/nullable v1.1.0 h1:eAh8JVc5430VtYVnq00Hrbpag9PFRGWLjxR1/3KntMs= -github.com/oapi-codegen/nullable v1.1.0/go.mod h1:KUZ3vUzkmEKY90ksAmit2+5juDIhIZhfDl+0PwOQlFY= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= diff --git a/apps/web/package.json b/apps/web/package.json index cfccd886..b0ea75a4 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -36,6 +36,7 @@ "@tanstack/react-table": "^8.21.3", "axios": "^1.9.0", "clsx": "^2.1.1", + "date-fns": "^4.1.0", "js-cookie": "^3.0.5", "ky": "^1.8.1", "nanoid": "^5.1.5", diff --git a/apps/web/pnpm-lock.yaml b/apps/web/pnpm-lock.yaml index 78733839..0699ca39 100644 --- a/apps/web/pnpm-lock.yaml +++ b/apps/web/pnpm-lock.yaml @@ -38,6 +38,9 @@ importers: clsx: specifier: ^2.1.1 version: 2.1.1 + date-fns: + specifier: ^4.1.0 + version: 4.1.0 js-cookie: specifier: ^3.0.5 version: 3.0.5 @@ -2574,6 +2577,9 @@ packages: resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} engines: {node: '>=18'} + date-fns@4.1.0: + resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} + debug@4.4.1: resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} engines: {node: '>=6.0'} @@ -7377,6 +7383,8 @@ snapshots: whatwg-mimetype: 4.0.0 whatwg-url: 14.2.0 + date-fns@4.1.0: {} + debug@4.4.1(supports-color@10.0.0): dependencies: ms: 2.1.3 diff --git a/apps/web/src/features/Admin/EventManager/hooks/useAdminEvents.ts b/apps/web/src/features/Admin/EventManager/hooks/useAdminEvents.ts index 14e0e9f2..707b0946 100644 --- a/apps/web/src/features/Admin/EventManager/hooks/useAdminEvents.ts +++ b/apps/web/src/features/Admin/EventManager/hooks/useAdminEvents.ts @@ -8,7 +8,9 @@ type Events = operations["get-events"]["responses"]["200"]["content"]["application/json"]; export async function fetchEvents(): Promise { - const result = await api.get("events?include_unpublished=true").json(); + const result = await api + .get("events?include_unpublished=true") + .json(); console.log(result); return result; } diff --git a/apps/web/src/features/Event/components/EventButton.tsx b/apps/web/src/features/Event/components/EventButton.tsx index 3a29278b..172f42de 100644 --- a/apps/web/src/features/Event/components/EventButton.tsx +++ b/apps/web/src/features/Event/components/EventButton.tsx @@ -3,6 +3,7 @@ import { tv } from "tailwind-variants"; import applicationStatus from "../applicationStatus"; import { Button, button, type ButtonProps } from "@/components/ui/Button"; import { cn } from "@/utils/cn"; +import { useRouter } from "@tanstack/react-router"; type ApplicationStatusTypes = keyof typeof applicationStatus; @@ -25,11 +26,13 @@ export const eventButton = tv({ interface EventButtonProps extends ButtonProps { status: ApplicationStatusTypes; + eventId: string; text?: string; } const EventButton = ({ status: statusProp, + eventId, className, text, }: EventButtonProps) => { @@ -38,11 +41,64 @@ const EventButton = ({ variant: "skeleton", className, }); + const router = useRouter(); + + const onClick = () => { + switch (statusProp) { + case "accepted": + // Make API call and then navigate to dashboard with accepted query param + console.log("Accepted button clicked for event"); + router.navigate({ + to: `/events/${eventId}/dashboard`, + }); + break; + case "attending": + case "staff": + case "admin": + case "underReview": + // Navigate to the dashboard + router.navigate({ + to: `/events/${eventId}/dashboard`, + }); + break; + case "waitlisted": + // Navigate to the waitlist page + router.navigate({ + to: `/events/${eventId}/waitlist/info`, + }); + break; + case "rejected": + // Navigate to the event info page + router.navigate({ + to: `/events/${eventId}/rejected`, + }); + break; + case "notApplied": + // Navigate to the application page + router.navigate({ + to: `/events/${eventId}/application`, + }); + break; + case "notGoing": + // Navigate to the event info page + router.navigate({ + to: `/events/${eventId}/feedback/decline`, + }); + break; + case "completed": + // Navigate to the event info page + router.navigate({ + to: `/events/${eventId}/summary`, + }); + break; + } + }; return ( diff --git a/apps/web/src/features/Event/components/EventCard.tsx b/apps/web/src/features/Event/components/EventCard.tsx index b3b46a68..6a94b29a 100644 --- a/apps/web/src/features/Event/components/EventCard.tsx +++ b/apps/web/src/features/Event/components/EventCard.tsx @@ -70,15 +70,24 @@ const EventCard = ({ {status === "accepted" ? (
- +
) : ( - + )}
diff --git a/apps/web/src/features/Event/hooks/useEventsWithUserInfo.ts b/apps/web/src/features/Event/hooks/useEventsWithUserInfo.ts index 1a79b460..ae7f939b 100644 --- a/apps/web/src/features/Event/hooks/useEventsWithUserInfo.ts +++ b/apps/web/src/features/Event/hooks/useEventsWithUserInfo.ts @@ -1,6 +1,7 @@ import { api } from "@/lib/ky"; import type { operations } from "@/lib/openapi/schema"; import { useQuery } from "@tanstack/react-query"; +import { mapEventsAPIResponseToEventCardProps } from "../utils/mapper"; export const eventsQueryKey = ["events", "published"] as const; @@ -8,14 +9,17 @@ export type EventsWithUserInfo = operations["get-events"]["responses"]["200"]["content"]["application/json"]; export async function fetchEvents(): Promise { - const result = await api.get("events?include_unpublished=false").json(); + const result = await api + .get("events?include_unpublished=false") + .json(); return result; } -export function useAdminEvents() { +export function useEventsWithUserInfo() { return useQuery({ queryKey: eventsQueryKey, queryFn: fetchEvents, - staleTime: 1000 * 60 * 5, // 5 minutes + staleTime: 1000 * 60 * 5, // 5 minutes, + select: (data) => data.map(mapEventsAPIResponseToEventCardProps), }); } diff --git a/apps/web/src/features/Event/utils/mapper.ts b/apps/web/src/features/Event/utils/mapper.ts index d3851dd4..5aec83e9 100644 --- a/apps/web/src/features/Event/utils/mapper.ts +++ b/apps/web/src/features/Event/utils/mapper.ts @@ -21,22 +21,71 @@ function formatDateRange(start: Date, end: Date): string { return `${startMonth} ${startDay} - ${endMonth} ${endDay}`; } -function mapEventsAPIResponseToEventCardProps(data: EventWithUserInfo): EventCardProps { - let status: keyof typeof applicationStatus = "notApplied" // Default - - if (!data.application_status) { - return { - eventId: data.id, - status, - title: data.name, - description: data.description ?? "No description", - date: - } - } +export function mapEventsAPIResponseToEventCardProps( + data: EventWithUserInfo, +): EventCardProps { + let status: keyof typeof applicationStatus = "notApplied"; // Default - switch (data.application_status?.application_status) { - case "under_review" && data.application_status?.valid == true: + if (!data.application_status) { + return { + eventId: data.id, + status, + title: data.name, + description: data.description ?? "No description", + date: formatDateRange(new Date(data.start_time), new Date(data.end_time)), + location: data.location ?? "Unknown", + }; + } + // Handle roles cases + if (data.event_role?.event_role_type === "staff") { + status = "staff"; + } else if (data.event_role?.event_role_type === "admin") { + status = "admin"; + } else if (data.event_role?.event_role_type === "attendee") { + status = "attending"; + } else if (data.event_role?.event_role_type === "applicant") { + // Handle application cases + switch (data.application_status.application_status) { + case "accepted": + status = "accepted"; + break; + case "rejected": + status = "rejected"; + break; + case "waitlisted": + status = "waitlisted"; + break; + case "submitted": + case "under_review": + status = "underReview"; + break; + case "started": + status = "notApplied"; + break; + case "withdrawn": + status = "notGoing"; + break; + default: + status = "notApplied"; // Default case + break; } -} + } else { + // If no specific role or application status, default to notApplied + status = "notApplied"; + } + // Check if the event is completed + if (new Date(data.end_time) < new Date()) { + status = "completed"; + } + + return { + eventId: data.id, + status, + title: data.name, + description: data.description ?? "No description", + date: formatDateRange(new Date(data.start_time), new Date(data.end_time)), + location: data.location ?? "Unknown", + }; +} diff --git a/apps/web/src/lib/openapi/schema.d.ts b/apps/web/src/lib/openapi/schema.d.ts index 7025ee2c..7b32906b 100644 --- a/apps/web/src/lib/openapi/schema.d.ts +++ b/apps/web/src/lib/openapi/schema.d.ts @@ -4,763 +4,770 @@ */ export interface paths { - "/auth/callback": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * OAuth2 Auth Callback - * @description This route is used for OAuth authentication methods to verify and login/create an account. - */ - post: operations["post-v1-auth-callback"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/auth/me": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get Current User - * @description Get the currently authenticated user's information. - */ - get: operations["get-auth-me"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/events/{eventId}/interest": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** Make an interest submission for an event (email list) */ - post: operations["post-event-interest"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/events": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get events - * @description Gets events with a nullable event role for authenticated users. - */ - get: operations["get-events"]; - put?: never; - /** Create a new event */ - post: operations["post-event"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/events/{eventId}": { - parameters: { - query?: never; - header?: never; - path: { - eventId: string; - }; - cookie?: never; - }; - /** Get an event */ - get: operations["get-single-event"]; - put?: never; - post?: never; - /** Delete an event */ - delete: operations["delete-event"]; - options?: never; - head?: never; - /** Update an event */ - patch: operations["patch-event"]; - trace?: never; - }; - "/events/{eventId}/staff": { - parameters: { - query?: never; - header?: never; - path: { - eventId: string; - }; - cookie?: never; - }; - /** - * Get all staff users for an event - * @description Gets all users with role STAFF or ADMIN - */ - get: operations["get-event-staff"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/events/{eventId}/roles": { - parameters: { - query?: never; - header?: never; - path: { - eventId: string; - }; - cookie?: never; - }; - get?: never; - put?: never; - /** Change or add event role of a user */ - post: operations["post-event-role"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; + "/auth/callback": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; + get?: never; + put?: never; + /** + * OAuth2 Auth Callback + * @description This route is used for OAuth authentication methods to verify and login/create an account. + */ + post: operations["post-v1-auth-callback"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/auth/me": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Current User + * @description Get the currently authenticated user's information. + */ + get: operations["get-auth-me"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/events/{eventId}/interest": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Make an interest submission for an event (email list) */ + post: operations["post-event-interest"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/events": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get events + * @description Gets events with a nullable event role for authenticated users. + */ + get: operations["get-events"]; + put?: never; + /** Create a new event */ + post: operations["post-event"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/events/{eventId}": { + parameters: { + query?: never; + header?: never; + path: { + eventId: string; + }; + cookie?: never; + }; + /** Get an event */ + get: operations["get-single-event"]; + put?: never; + post?: never; + /** Delete an event */ + delete: operations["delete-event"]; + options?: never; + head?: never; + /** Update an event */ + patch: operations["patch-event"]; + trace?: never; + }; + "/events/{eventId}/staff": { + parameters: { + query?: never; + header?: never; + path: { + eventId: string; + }; + cookie?: never; + }; + /** + * Get all staff users for an event + * @description Gets all users with role STAFF or ADMIN + */ + get: operations["get-event-staff"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/events/{eventId}/roles": { + parameters: { + query?: never; + header?: never; + path: { + eventId: string; + }; + cookie?: never; + }; + get?: never; + put?: never; + /** Change or add event role of a user */ + post: operations["post-event-role"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; } export type webhooks = Record; export interface components { - schemas: { - Event: { - /** Format: uuid */ - id: string; - name: string; - description?: string | null; - location?: string | null; - /** Format: uri */ - location_url?: string | null; - /** Format: int32 */ - max_attendees?: number | null; - /** Format: date-time */ - application_open: string; - /** Format: date-time */ - application_close: string; - /** Format: date-time */ - rsvp_deadline?: string | null; - /** Format: date-time */ - decision_release?: string | null; - /** Format: date-time */ - start_time: string; - /** Format: date-time */ - end_time: string; - /** Format: uri */ - website_url?: string | null; - is_published?: boolean | null; - /** Format: date-time */ - created_at?: string | null; - /** Format: date-time */ - updated_at?: string | null; - }; - /** - * ErrorResponse - * @description This model is returned on server errors, it returns an error code (lookup code definitions in documentation), an error key, and a message. - */ - ErrorResponse: { - error: string; - message: string; - }; - /** - * UserContext - * @description This is the model used when returning from GetMe. Used often in middleware! - */ - UserContext: { - /** Format: uuid */ - userId: string; - name: string; - onboarded: boolean; - /** Format: uri */ - image?: string | null; - role: components["schemas"]["PlatformRole"]; - }; - /** - * PlatformRole - * @description A user's role on the platform. Either base permissions or elevated superuser perms. - * @enum {string} - */ - PlatformRole: "user" | "superuser"; - /** Session */ - Session: { - /** Format: uuid */ - id: string; - /** Format: uuid */ - user_id: string; - /** Format: date-time */ - expires_at: string; - ip_address?: string | null; - user_agent?: string | null; - }; - User: { - /** - * Format: uuid - * @example 123e4567-e89b-12d3-a456-426614174000 - */ - id: string; - /** @example John Doe */ - name: string; - /** - * Format: email - * @example john@example.com - */ - email?: string | null; - /** @example true */ - email_verified: boolean; - /** @example false */ - onboarded: boolean; - /** - * Format: uri - * @example https://example.com/avatar.jpg - */ - image?: string | null; - /** - * Format: date-time - * @example 2025-08-08T17:00:00Z - */ - created_at: string; - /** - * Format: date-time - * @example 2025-08-08T18:00:00Z - */ - updated_at: string; - role: components["schemas"]["UserRole"]; - }; - UserWithEventRole: components["schemas"]["User"] & { - event_role: components["schemas"]["EventRole"]; - }; - /** - * @example user - * @enum {string} - */ - UserRole: "user" | "superuser"; - /** - * @example attendee - * @enum {string} - */ - EventRole: "admin" | "staff" | "attendee" | "applicant"; - /** - * @example under_review - * @enum {string} - */ - ApplicationStatus: "started" | "submitted" | "under_review" | "accepted" | "rejected" | "waitlisted" | "withdrawn"; - EventWithUserInfo: components["schemas"]["Event"] & { - event_role?: { - event_role_type: components["schemas"]["EventRole"]; - valid: boolean; - }; - application_status?: { - application_status: components["schemas"]["ApplicationStatus"]; - valid: boolean; - }; - }; + schemas: { + Event: { + /** Format: uuid */ + id: string; + name: string; + description?: string | null; + location?: string | null; + /** Format: uri */ + location_url?: string | null; + /** Format: int32 */ + max_attendees?: number | null; + /** Format: date-time */ + application_open: string; + /** Format: date-time */ + application_close: string; + /** Format: date-time */ + rsvp_deadline?: string | null; + /** Format: date-time */ + decision_release?: string | null; + /** Format: date-time */ + start_time: string; + /** Format: date-time */ + end_time: string; + /** Format: uri */ + website_url?: string | null; + is_published?: boolean | null; + /** Format: date-time */ + created_at?: string | null; + /** Format: date-time */ + updated_at?: string | null; }; - responses: { - /** @description Unauthenticated: Requester is not currently authenticated. */ - Unauthenticated: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ErrorResponse"]; - }; - }; - /** @description Unauthorized: User does not have access to this resource. */ - Unauthorized: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ErrorResponse"]; - }; - }; - "Bad-Request": { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - parameters: never; - requestBodies: never; - headers: never; - pathItems: never; + /** + * ErrorResponse + * @description This model is returned on server errors, it returns an error code (lookup code definitions in documentation), an error key, and a message. + */ + ErrorResponse: { + error: string; + message: string; + }; + /** + * UserContext + * @description This is the model used when returning from GetMe. Used often in middleware! + */ + UserContext: { + /** Format: uuid */ + userId: string; + name: string; + onboarded: boolean; + /** Format: uri */ + image?: string | null; + role: components["schemas"]["PlatformRole"]; + }; + /** + * PlatformRole + * @description A user's role on the platform. Either base permissions or elevated superuser perms. + * @enum {string} + */ + PlatformRole: "user" | "superuser"; + /** Session */ + Session: { + /** Format: uuid */ + id: string; + /** Format: uuid */ + user_id: string; + /** Format: date-time */ + expires_at: string; + ip_address?: string | null; + user_agent?: string | null; + }; + User: { + /** + * Format: uuid + * @example 123e4567-e89b-12d3-a456-426614174000 + */ + id: string; + /** @example John Doe */ + name: string; + /** + * Format: email + * @example john@example.com + */ + email?: string | null; + /** @example true */ + email_verified: boolean; + /** @example false */ + onboarded: boolean; + /** + * Format: uri + * @example https://example.com/avatar.jpg + */ + image?: string | null; + /** + * Format: date-time + * @example 2025-08-08T17:00:00Z + */ + created_at: string; + /** + * Format: date-time + * @example 2025-08-08T18:00:00Z + */ + updated_at: string; + role: components["schemas"]["UserRole"]; + }; + UserWithEventRole: components["schemas"]["User"] & { + event_role: components["schemas"]["EventRole"]; + }; + /** + * @example user + * @enum {string} + */ + UserRole: "user" | "superuser"; + /** + * @example attendee + * @enum {string} + */ + EventRole: "admin" | "staff" | "attendee" | "applicant"; + /** + * @example under_review + * @enum {string} + */ + ApplicationStatus: + | "started" + | "submitted" + | "under_review" + | "accepted" + | "rejected" + | "waitlisted" + | "withdrawn"; + EventWithUserInfo: components["schemas"]["Event"] & { + event_role?: { + event_role_type: components["schemas"]["EventRole"]; + valid: boolean; + }; + application_status?: { + application_status: components["schemas"]["ApplicationStatus"]; + valid: boolean; + }; + }; + }; + responses: { + /** @description Unauthenticated: Requester is not currently authenticated. */ + Unauthenticated: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Unauthorized: User does not have access to this resource. */ + Unauthorized: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + "Bad-Request": { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; } export type $defs = Record; export interface operations { - "post-v1-auth-callback": { - parameters: { - query: { - /** @description The OAuth code passed back from the provider. Part of the PKCE flow. */ - code: string; - /** @description The state containing a base64 encoded version of the nonce, provider, and redirect url. */ - state: string; - }; - header?: never; - path?: never; - cookie: { - /** @description The nonce for comparing against the callback state decoded to prevent CSRF attacks. */ - sh_auth_nonce: string; - }; - }; - requestBody?: never; - responses: { - /** @description OK: User is logged in successfully. For when the redirect field is empty. */ - 200: { - headers: { - /** @description Sets a sh_session cookie to signify auth status */ - "Set-Cookie"?: string; - [name: string]: unknown; - }; - content?: never; - }; - /** @description Found: Logged in and redirected to a requested location */ - 302: { - headers: { - /** @description Sets a sh_session cookie to signify auth status */ - "Set-Cookie"?: string; - [name: string]: unknown; - }; - content?: never; - }; - /** @description Bad Request: Something went wrong with the request queries or their properties */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ErrorResponse"]; - }; - }; - /** @description Forbidden: Something went wrong verifying identity or authenticating. */ - 403: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ErrorResponse"]; - }; - }; - /** @description Internal Server Error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ErrorResponse"]; - }; - }; - /** @description Bad Gateway: Authenticating OAuth server did not respond or user does not exist */ - 502: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ErrorResponse"]; - }; - }; - }; - }; - "get-auth-me": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: { - /** @description The authenticated session token/id */ - sh_session?: string; - }; - }; - requestBody?: never; - responses: { - /** @description OK: Current user data returned */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["UserContext"]; - }; - }; - 401: components["responses"]["Unauthenticated"]; - /** @description Server Error: Something went terribly wrong on our end. */ - "5XX": { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ErrorResponse"]; - }; - }; - }; - }; - "post-event-interest": { - parameters: { - query?: never; - header?: never; - path: { - eventId: string; - }; - cookie?: never; - }; - requestBody: { - content: { - "application/json": { - /** - * Format: email - * @example johndoe@ufl.edu - */ - email: string; - /** @example SHX Frontpage */ - source?: string; - }; - }; - }; - responses: { - /** @description OK: Interest email created */ - 201: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Bad request/Malformed request */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ErrorResponse"]; - }; - }; - /** @description Duplicate email found in DB */ - 409: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ErrorResponse"]; - }; - }; - /** @description Server Error: Something went terribly wrong on our end. */ - "5XX": { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ErrorResponse"]; - }; - }; - }; - }; - "get-events": { - parameters: { - query?: { - /** @description If true, include unpublished events as well. Superusers ONLY. */ - include_published?: boolean; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description OK: Events returned */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["EventWithUserInfo"][]; - }; - }; - }; - }; - "post-event": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": { - /** @example Open Software Club's Workshop */ - name: string; - /** - * Format: date-time - * @example 2025-08-01T08:00:00Z - */ - application_open: string; - /** - * Format: date-time - * @example 2025-12-01T023:59:00Z - */ - application_close: string; - /** - * Format: date-time - * @example 2026-02-10T17:00:00Z - */ - start_time: string; - /** - * Format: date-time - * @example 2026-02-12T08:00:00Z - */ - end_time: string; - description?: string; - location?: string; - location_url?: string; - max_attendees?: number; - /** - * Format: date-time - * @example 2025-08-01T08:00:00Z - */ - rsvp_deadline?: string; - /** - * Format: date-time - * @example 2025-08-01T08:00:00Z - */ - decision_release?: string; - website_url?: string; - is_published?: boolean; - }; - }; - }; - responses: { - /** @description OK: Event created */ - 201: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": unknown; - }; - }; - /** @description Bad request/Malformed request */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": unknown; - examples: unknown; - }; - }; - /** @description endTime is before startTime or applicationClose is before applicationOpen */ - 409: { - headers: { - [name: string]: unknown; - }; - content: { - schema: unknown; - examples: unknown; - }; - }; - /** @description Server Error: Something went terribly wrong on our end. */ - "5XX": { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ErrorResponse"]; - }; - }; - }; - }; - "get-single-event": { - parameters: { - query?: never; - header?: never; - path: { - eventId: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description OK - Event received */ - 201: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["Event"]; - }; - }; - /** @description Server Error: Something went terribly wrong on our end. */ - "5XX": { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ErrorResponse"]; - }; - }; - }; - }; - "delete-event": { - parameters: { - query?: never; - header?: never; - path: { - eventId: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description OK - Event deleted (patched) */ - 204: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Server Error: Something went terribly wrong on our end. */ - "5XX": { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ErrorResponse"]; - }; - }; - }; - }; - "patch-event": { - parameters: { - query?: never; - header?: never; - path: { - eventId: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description OK - Event updated (patched) */ - 204: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Server Error: Something went terribly wrong on our end. */ - "5XX": { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ErrorResponse"]; - }; - }; - }; - }; - "get-event-staff": { - parameters: { - query?: never; - header?: never; - path: { - eventId: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description OK - Return users */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["UserWithEventRole"][]; - }; - }; - /** @description Server Error: Something went terribly wrong on our end. */ - "5XX": { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ErrorResponse"]; - }; - }; - }; - }; - "post-event-role": { - parameters: { - query?: never; - header?: never; - path: { - eventId: string; - }; - cookie?: never; - }; - requestBody: { - content: { - "application/json": { - /** @example user@example.com */ - email?: string | null; - /** - * Format: uuid - * @example 123e4567-e89b-12d3-a456-426614174000 - */ - user_id?: string | null; - role: components["schemas"]["EventRole"]; - }; - }; - }; - responses: { - /** @description OK - Return users */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Not Found - User not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ErrorResponse"]; - }; - }; - /** @description Server Error: Something went terribly wrong on our end. */ - "5XX": { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ErrorResponse"]; - }; - }; + "post-v1-auth-callback": { + parameters: { + query: { + /** @description The OAuth code passed back from the provider. Part of the PKCE flow. */ + code: string; + /** @description The state containing a base64 encoded version of the nonce, provider, and redirect url. */ + state: string; + }; + header?: never; + path?: never; + cookie: { + /** @description The nonce for comparing against the callback state decoded to prevent CSRF attacks. */ + sh_auth_nonce: string; + }; + }; + requestBody?: never; + responses: { + /** @description OK: User is logged in successfully. For when the redirect field is empty. */ + 200: { + headers: { + /** @description Sets a sh_session cookie to signify auth status */ + "Set-Cookie"?: string; + [name: string]: unknown; + }; + content?: never; + }; + /** @description Found: Logged in and redirected to a requested location */ + 302: { + headers: { + /** @description Sets a sh_session cookie to signify auth status */ + "Set-Cookie"?: string; + [name: string]: unknown; + }; + content?: never; + }; + /** @description Bad Request: Something went wrong with the request queries or their properties */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Forbidden: Something went wrong verifying identity or authenticating. */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Bad Gateway: Authenticating OAuth server did not respond or user does not exist */ + 502: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + "get-auth-me": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: { + /** @description The authenticated session token/id */ + sh_session?: string; + }; + }; + requestBody?: never; + responses: { + /** @description OK: Current user data returned */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UserContext"]; + }; + }; + 401: components["responses"]["Unauthenticated"]; + /** @description Server Error: Something went terribly wrong on our end. */ + "5XX": { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + "post-event-interest": { + parameters: { + query?: never; + header?: never; + path: { + eventId: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** + * Format: email + * @example johndoe@ufl.edu + */ + email: string; + /** @example SHX Frontpage */ + source?: string; + }; + }; + }; + responses: { + /** @description OK: Interest email created */ + 201: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Bad request/Malformed request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Duplicate email found in DB */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Server Error: Something went terribly wrong on our end. */ + "5XX": { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + "get-events": { + parameters: { + query?: { + /** @description If true, include unpublished events as well. Superusers ONLY. */ + include_published?: boolean; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK: Events returned */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["EventWithUserInfo"][]; + }; + }; + }; + }; + "post-event": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** @example Open Software Club's Workshop */ + name: string; + /** + * Format: date-time + * @example 2025-08-01T08:00:00Z + */ + application_open: string; + /** + * Format: date-time + * @example 2025-12-01T023:59:00Z + */ + application_close: string; + /** + * Format: date-time + * @example 2026-02-10T17:00:00Z + */ + start_time: string; + /** + * Format: date-time + * @example 2026-02-12T08:00:00Z + */ + end_time: string; + description?: string; + location?: string; + location_url?: string; + max_attendees?: number; + /** + * Format: date-time + * @example 2025-08-01T08:00:00Z + */ + rsvp_deadline?: string; + /** + * Format: date-time + * @example 2025-08-01T08:00:00Z + */ + decision_release?: string; + website_url?: string; + is_published?: boolean; + }; + }; + }; + responses: { + /** @description OK: Event created */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Bad request/Malformed request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + examples: unknown; + }; + }; + /** @description endTime is before startTime or applicationClose is before applicationOpen */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + schema: unknown; + examples: unknown; + }; + }; + /** @description Server Error: Something went terribly wrong on our end. */ + "5XX": { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + "get-single-event": { + parameters: { + query?: never; + header?: never; + path: { + eventId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK - Event received */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Event"]; }; + }; + /** @description Server Error: Something went terribly wrong on our end. */ + "5XX": { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + "delete-event": { + parameters: { + query?: never; + header?: never; + path: { + eventId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK - Event deleted (patched) */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Server Error: Something went terribly wrong on our end. */ + "5XX": { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + "patch-event": { + parameters: { + query?: never; + header?: never; + path: { + eventId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK - Event updated (patched) */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Server Error: Something went terribly wrong on our end. */ + "5XX": { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + "get-event-staff": { + parameters: { + query?: never; + header?: never; + path: { + eventId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK - Return users */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UserWithEventRole"][]; + }; + }; + /** @description Server Error: Something went terribly wrong on our end. */ + "5XX": { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + "post-event-role": { + parameters: { + query?: never; + header?: never; + path: { + eventId: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** @example user@example.com */ + email?: string | null; + /** + * Format: uuid + * @example 123e4567-e89b-12d3-a456-426614174000 + */ + user_id?: string | null; + role: components["schemas"]["EventRole"]; + }; + }; + }; + responses: { + /** @description OK - Return users */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Not Found - User not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Server Error: Something went terribly wrong on our end. */ + "5XX": { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; }; + }; } diff --git a/apps/web/src/lib/openapi/types.ts b/apps/web/src/lib/openapi/types.ts index d3c10b50..bbe30144 100644 --- a/apps/web/src/lib/openapi/types.ts +++ b/apps/web/src/lib/openapi/types.ts @@ -8,4 +8,4 @@ export type Event = components["schemas"]["Event"]; export type CreateEvent = operations["post-event"]["requestBody"]["content"]["application/json"]; export type User = components["schemas"]["User"]; -export type EventWithUserInfo = components["schemas"]["EventWithUserInfo"] +export type EventWithUserInfo = components["schemas"]["EventWithUserInfo"]; diff --git a/apps/web/src/routes/_main/portal.tsx b/apps/web/src/routes/_main/portal.tsx index e3926c49..e88df869 100644 --- a/apps/web/src/routes/_main/portal.tsx +++ b/apps/web/src/routes/_main/portal.tsx @@ -1,8 +1,7 @@ -import { Button } from "@/components/ui/Button"; import { EventCard } from "@/features/Event/components/EventCard"; -import { auth } from "@/lib/authClient"; +import { useEventsWithUserInfo } from "@/features/Event/hooks/useEventsWithUserInfo"; import { createFileRoute } from "@tanstack/react-router"; -import { Heading } from "react-aria-components"; +import { Heading, Text } from "react-aria-components"; export const Route = createFileRoute("/_main/portal")({ component: RouteComponent, @@ -10,14 +9,52 @@ export const Route = createFileRoute("/_main/portal")({ function RouteComponent() { const { user } = Route.useRouteContext(); - const logout = async () => { - try { - await auth.logOut(); - window.location.href = "/"; - } catch (error) { - console.error("Error during logout:", error); - } - }; + const { data, isLoading, isError } = useEventsWithUserInfo(); + + if (isLoading) { + return ( +
+
+ + Welcome, {user?.name ?? "hacker"}! + +

+ Ready to start hacking? +

+
+ +
+ {[...Array(3)].map((_, i) => ( +
+ ))} +
+
+ ); + } + + if (isError || data === undefined) { + return ( +
+
+ + Welcome, {user?.name ?? "hacker"}! + +

+ Ready to start hacking? +

+
+ +
+ + Whoops, something went wrong, please refresh and try again! + +
+
+ ); + } return (
@@ -29,32 +66,15 @@ function RouteComponent() {
- - - - - + {data.map((event) => ( + + ))} + + {data.length === 0 && ( + + Awww such empty. Please check back later for events! + + )}
); diff --git a/apps/web/src/routes/events/$eventId/dashboard/index.tsx b/apps/web/src/routes/events/$eventId/dashboard/index.tsx new file mode 100644 index 00000000..b72eb762 --- /dev/null +++ b/apps/web/src/routes/events/$eventId/dashboard/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute("/events/$eventId/dashboard/")({ + component: RouteComponent, +}); + +function RouteComponent() { + return
Hello "/events/$eventId/dashboard/"!
; +} diff --git a/apps/web/src/routes/events/$eventId/feedback/declined.tsx b/apps/web/src/routes/events/$eventId/feedback/declined.tsx new file mode 100644 index 00000000..5002b26f --- /dev/null +++ b/apps/web/src/routes/events/$eventId/feedback/declined.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute("/events/$eventId/feedback/declined")({ + component: RouteComponent, +}); + +function RouteComponent() { + return
Hello "/events/$eventId/feedback/declined"!
; +} diff --git a/apps/web/src/routes/events/$eventId/rejected.tsx b/apps/web/src/routes/events/$eventId/rejected.tsx new file mode 100644 index 00000000..07a3811b --- /dev/null +++ b/apps/web/src/routes/events/$eventId/rejected.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute("/events/$eventId/rejected")({ + component: RouteComponent, +}); + +function RouteComponent() { + return
Hello "/events/$eventId/rejected"!
; +} diff --git a/apps/web/src/routes/events/$eventId/summary.tsx b/apps/web/src/routes/events/$eventId/summary.tsx new file mode 100644 index 00000000..f0d56f22 --- /dev/null +++ b/apps/web/src/routes/events/$eventId/summary.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute("/events/$eventId/summary")({ + component: RouteComponent, +}); + +function RouteComponent() { + return
Hello "/events/$eventId/summary"!
; +} diff --git a/apps/web/src/routes/events/$eventId/waitlist/info.tsx b/apps/web/src/routes/events/$eventId/waitlist/info.tsx new file mode 100644 index 00000000..649cd013 --- /dev/null +++ b/apps/web/src/routes/events/$eventId/waitlist/info.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute("/events/$eventId/waitlist/info")({ + component: RouteComponent, +}); + +function RouteComponent() { + return
Hello "/events/$eventId/waitlist/info"!
; +}