diff --git a/apps/api/.gitignore b/apps/api/.gitignore index a16add54..4007624e 100644 --- a/apps/api/.gitignore +++ b/apps/api/.gitignore @@ -1,2 +1,3 @@ .env tmp +docs/openapi.json \ No newline at end of file diff --git a/apps/api/cmd/BAT_worker/main.go b/apps/api/cmd/BAT_worker/main.go index ac98ca19..bc8bf15d 100644 --- a/apps/api/cmd/BAT_worker/main.go +++ b/apps/api/cmd/BAT_worker/main.go @@ -1,23 +1,5 @@ package main -import ( - "time" - - "github.com/hibiken/asynq" - "github.com/swamphacks/core/apps/api/internal/config" - "github.com/swamphacks/core/apps/api/internal/database" - "github.com/swamphacks/core/apps/api/internal/database/repository" - "github.com/swamphacks/core/apps/api/internal/domains/application" - "github.com/swamphacks/core/apps/api/internal/domains/bat" - "github.com/swamphacks/core/apps/api/internal/domains/email" - "github.com/swamphacks/core/apps/api/internal/domains/hackathon" - "github.com/swamphacks/core/apps/api/internal/emailutils" - "github.com/swamphacks/core/apps/api/internal/logger" - "github.com/swamphacks/core/apps/api/internal/storage" - "github.com/swamphacks/core/apps/api/internal/tasks" - "github.com/swamphacks/core/apps/api/internal/workers" -) - /* -. .- _..-'( )`-.._ @@ -37,70 +19,69 @@ V V V }' `\ /' `{ V V V */ func main() { - logger := logger.New() - cfg := config.LoadConfig() - - redisOpt, err := asynq.ParseRedisURI(cfg.RedisURL) - if err != nil { - logger.Fatal().Msg("failed to parse REDIS_URL") - } - - srv := asynq.NewServer( - redisOpt, - asynq.Config{ - Concurrency: 1, - Queues: map[string]int{ - "bat": 1, - }, - TaskCheckInterval: 10 * time.Second, - DelayedTaskCheckInterval: time.Minute, - HealthCheckInterval: 2 * time.Minute, - JanitorInterval: time.Hour, - JanitorBatchSize: 100, - }, - ) - - schedulerLocation, err := time.LoadLocation("America/New_York") - if err != nil { - panic(err) - } - scheduler := asynq.NewScheduler( - redisOpt, - &asynq.SchedulerOpts{ - Location: schedulerLocation, - }, - ) - - taskQueueClient := asynq.NewClient(redisOpt) - defer taskQueueClient.Close() - - db := database.NewDB(cfg.DatabaseURL) - defer db.Close() - - txm := database.NewTransactionManager(db) - - hackathonRepo := repository.NewHackathonRepository(db) - applicationRepo := repository.NewApplicationRepository(db) - userRepo := repository.NewUserRepository(db) - batRunsRepo := repository.NewBatRunsRepository(db) - eventInterestsRepo := repository.NewEventInterestsRepository(db) - - sesClient := emailutils.NewSESClient(cfg.AWS.AccessKey, cfg.AWS.AccessKeySecret, cfg.AWS.Region, logger) - emailService := email.NewEmailService(hackathonRepo, userRepo, taskQueueClient, sesClient, nil, logger, cfg) - batService := bat.NewBatService(applicationRepo, hackathonRepo, userRepo, batRunsRepo, emailService, txm, nil, scheduler, cfg, logger) - applicationService := application.NewService(applicationRepo, userRepo, hackathonRepo, txm, nil, nil, scheduler, emailService, batService, cfg, logger) - hackathonService := hackathon.NewService(hackathonRepo, userRepo, eventInterestsRepo, &storage.R2Client{}, nil, logger) - - BATWorker := workers.NewBATWorker(batService, applicationService, hackathonService, scheduler, taskQueueClient, cfg, logger) - - mux := asynq.NewServeMux() - mux.HandleFunc(tasks.TypeCalculateAdmissions, BATWorker.HandleCalculateAdmissionsTask) - mux.HandleFunc(tasks.TypeTransitionWaitlist, BATWorker.HandleTransitionWaitlistTask) - mux.HandleFunc(tasks.TypeScheduleTransitionWaitlist, BATWorker.HandleScheduleTransitionWaitlistTask) - mux.HandleFunc(tasks.TypeShutdownScheduler, BATWorker.HandleShutdownScheduler) - - if err := srv.Run(mux); err != nil { - logger.Fatal().Msg("Failed to run BAT worker") - } - + // logger := logger.New() + // cfg := config.LoadConfig() + + // redisOpt, err := asynq.ParseRedisURI(cfg.RedisURL) + // if err != nil { + // logger.Fatal().Msg("failed to parse REDIS_URL") + // } + + // srv := asynq.NewServer( + // redisOpt, + // asynq.Config{ + // Concurrency: 1, + // Queues: map[string]int{ + // "bat": 1, + // }, + // TaskCheckInterval: 10 * time.Second, + // DelayedTaskCheckInterval: time.Minute, + // HealthCheckInterval: 2 * time.Minute, + // JanitorInterval: time.Hour, + // JanitorBatchSize: 100, + // }, + // ) + + // schedulerLocation, err := time.LoadLocation("America/New_York") + // if err != nil { + // panic(err) + // } + // scheduler := asynq.NewScheduler( + // redisOpt, + // &asynq.SchedulerOpts{ + // Location: schedulerLocation, + // }, + // ) + + // taskQueueClient := asynq.NewClient(redisOpt) + // defer taskQueueClient.Close() + + // db := database.NewDB(cfg.DatabaseURL) + // defer db.Close() + + // txm := database.NewTransactionManager(db) + + // hackathonRepo := repository.NewHackathonRepository(db) + // applicationRepo := repository.NewApplicationRepository(db) + // userRepo := repository.NewUserRepository(db) + // batRunsRepo := repository.NewBatRunsRepository(db) + // eventInterestsRepo := repository.NewEventInterestsRepository(db) + + // sesClient := emailutils.NewSESClient(cfg.AWS.AccessKey, cfg.AWS.AccessKeySecret, cfg.AWS.Region, logger) + // emailService := email.NewEmailService(hackathonRepo, userRepo, taskQueueClient, sesClient, nil, logger, cfg) + // batService := bat.NewBatService(applicationRepo, hackathonRepo, userRepo, batRunsRepo, emailService, txm, nil, scheduler, cfg, logger) + // applicationService := application.NewService(applicationRepo, userRepo, hackathonRepo, txm, nil, nil, scheduler, emailService, batService, cfg, logger) + // hackathonService := hackathon.NewService(hackathonRepo, userRepo, eventInterestsRepo, &storage.R2Client{}, nil, logger) + + // BATWorker := workers.NewBATWorker(batService, applicationService, hackathonService, scheduler, taskQueueClient, cfg, logger) + + // mux := asynq.NewServeMux() + // mux.HandleFunc(tasks.TypeCalculateAdmissions, BATWorker.HandleCalculateAdmissionsTask) + // mux.HandleFunc(tasks.TypeTransitionWaitlist, BATWorker.HandleTransitionWaitlistTask) + // mux.HandleFunc(tasks.TypeScheduleTransitionWaitlist, BATWorker.HandleScheduleTransitionWaitlistTask) + // mux.HandleFunc(tasks.TypeShutdownScheduler, BATWorker.HandleShutdownScheduler) + + // if err := srv.Run(mux); err != nil { + // logger.Fatal().Msg("Failed to run BAT worker") + // } } diff --git a/apps/api/cmd/samples/main.go b/apps/api/cmd/samples/main.go index bb068bda..f917369f 100644 --- a/apps/api/cmd/samples/main.go +++ b/apps/api/cmd/samples/main.go @@ -16,7 +16,7 @@ func main() { hackathonRepo := repository.NewHackathonRepository(db) - appOpenTime := time.Date(2026, 5, 26, 19, 13, 20, 0, time.UTC) + appOpenTime := time.Date(2026, 4, 26, 19, 13, 20, 0, time.UTC) appCloseTime := time.Date(2026, 6, 26, 19, 13, 20, 0, time.UTC) earlyAppOpenTime := time.Date(2026, 4, 20, 19, 13, 20, 0, time.UTC) earlyAppCloseTime := time.Date(2026, 4, 26, 19, 13, 20, 0, time.UTC) diff --git a/apps/api/docs/openapi.json b/apps/api/docs/openapi.json deleted file mode 100644 index c532b40e..00000000 --- a/apps/api/docs/openapi.json +++ /dev/null @@ -1 +0,0 @@ -{"components":{"schemas":{"ApplicationStatistics":{"additionalProperties":false,"properties":{"ageStats":{"$ref":"#/components/schemas/GetApplicationAgeSplitRow"},"genderStats":{"$ref":"#/components/schemas/GetApplicationGenderSplitRow"},"majorStats":{"items":{"$ref":"#/components/schemas/GetApplicationMajorSplitRow"},"type":["array","null"]},"raceStats":{"items":{"$ref":"#/components/schemas/GetApplicationRaceSplitRow"},"type":["array","null"]},"schoolStats":{"items":{"$ref":"#/components/schemas/GetApplicationSchoolSplitRow"},"type":["array","null"]},"statusStats":{"$ref":"#/components/schemas/GetApplicationStatusSplitRow"}},"required":["genderStats","ageStats","raceStats","majorStats","schoolStats","statusStats"],"type":"object"},"AssignRoleBatchRequest":{"additionalProperties":false,"properties":{"assignments":{"items":{"$ref":"#/components/schemas/AssignRoleRequest"},"type":["array","null"]}},"required":["assignments"],"type":"object"},"AssignRoleRequest":{"additionalProperties":false,"properties":{"email":{"type":["string","null"]},"role":{"type":"string"},"userID":{"type":["string","null"]}},"required":["email","userID","role"],"type":"object"},"AssignedApplication":{"additionalProperties":false,"properties":{"applicantId":{"type":"string"},"status":{"type":"string"}},"required":["applicantId","status"],"type":"object"},"CheckInRequest":{"additionalProperties":false,"properties":{"rfid":{"type":["string","null"]},"userID":{"type":"string"}},"required":["userID","rfid"],"type":"object"},"CreateJoinRequest":{"additionalProperties":false,"properties":{"message":{"type":["string","null"]}},"required":["message"],"type":"object"},"CreateRedeemableRequest":{"additionalProperties":false,"properties":{"amount":{"format":"int64","minimum":1,"type":"integer"},"maxUserAmount":{"format":"int64","type":"integer"},"name":{"minLength":1,"type":"string"}},"required":["name","amount","maxUserAmount"],"type":"object"},"CreateTeamRequest":{"additionalProperties":false,"properties":{"name":{"type":"string"}},"required":["name"],"type":"object"},"ErrorDetail":{"additionalProperties":false,"properties":{"location":{"description":"Where the error occurred, e.g. 'body.items[3].tags' or 'path.thing-id'","type":"string"},"message":{"description":"Error message text","type":"string"},"value":{"description":"The value at the given location"}},"type":"object"},"ErrorModel":{"additionalProperties":false,"properties":{"detail":{"description":"A human-readable explanation specific to this occurrence of the problem.","examples":["Property foo is required but is missing."],"type":"string"},"errors":{"description":"Optional list of individual error details","items":{"$ref":"#/components/schemas/ErrorDetail"},"type":["array","null"]},"instance":{"description":"A URI reference that identifies the specific occurrence of the problem.","examples":["https://example.com/error-log/abc123"],"format":"uri","type":"string"},"status":{"description":"HTTP status code","examples":[400],"format":"int64","type":"integer"},"title":{"description":"A short, human-readable summary of the problem type. This value should not change between occurrences of the error.","examples":["Bad Request"],"type":"string"},"type":{"default":"about:blank","description":"A URI reference to human-readable documentation for the error.","examples":["https://example.com/errors/example"],"format":"uri","type":"string"}},"type":"object"},"FormFile":{"additionalProperties":false,"properties":{"ContentType":{"type":"string"},"Filename":{"type":"string"},"IsSet":{"type":"boolean"},"Size":{"format":"int64","type":"integer"}},"required":["ContentType","IsSet","Size","Filename"],"type":"object"},"GetApplicationAgeSplitRow":{"additionalProperties":false,"properties":{"age_18":{"format":"int64","type":"integer"},"age_19":{"format":"int64","type":"integer"},"age_20":{"format":"int64","type":"integer"},"age_21":{"format":"int64","type":"integer"},"age_22":{"format":"int64","type":"integer"},"age_23_plus":{"format":"int64","type":"integer"},"underage":{"format":"int64","type":"integer"}},"required":["underage","age_18","age_19","age_20","age_21","age_22","age_23_plus"],"type":"object"},"GetApplicationGenderSplitRow":{"additionalProperties":false,"properties":{"female":{"format":"int64","type":"integer"},"male":{"format":"int64","type":"integer"},"non_binary":{"format":"int64","type":"integer"},"other":{"format":"int64","type":"integer"}},"required":["male","female","non_binary","other"],"type":"object"},"GetApplicationMajorSplitRow":{"additionalProperties":false,"properties":{"count":{"format":"int64","type":"integer"},"major":{"type":"string"}},"required":["major","count"],"type":"object"},"GetApplicationRaceSplitRow":{"additionalProperties":false,"properties":{"count":{"format":"int64","type":"integer"},"race_group":{"type":"string"}},"required":["race_group","count"],"type":"object"},"GetApplicationSchoolSplitRow":{"additionalProperties":false,"properties":{"count":{"format":"int64","type":"integer"},"school":{"type":"string"}},"required":["school","count"],"type":"object"},"GetApplicationStatusSplitRow":{"additionalProperties":false,"properties":{"accepted":{"format":"int64","type":"integer"},"rejected":{"format":"int64","type":"integer"},"started":{"format":"int64","type":"integer"},"submitted":{"format":"int64","type":"integer"},"under_review":{"format":"int64","type":"integer"},"waitlisted":{"format":"int64","type":"integer"},"withdrawn":{"format":"int64","type":"integer"}},"required":["started","submitted","under_review","accepted","rejected","waitlisted","withdrawn"],"type":"object"},"GetAttendeesWithDiscordRow":{"additionalProperties":false,"properties":{"discord_id":{"type":"string"},"email":{"type":["string","null"]},"name":{"type":"string"},"user_id":{"type":"string"}},"required":["discord_id","user_id","name","email"],"type":"object"},"GetRedeemablesRow":{"additionalProperties":false,"properties":{"created_at":{"format":"date-time","type":"string"},"id":{"type":"string"},"max_user_amount":{"format":"int32","type":"integer"},"name":{"type":"string"},"total_redeemed":{},"total_stock":{"format":"int32","type":"integer"},"updated_at":{"format":"date-time","type":"string"}},"required":["id","name","total_stock","max_user_amount","created_at","updated_at","total_redeemed"],"type":"object"},"Hackathon":{"additionalProperties":false,"properties":{"accept_early_applications":{"type":"boolean"},"application_close":{"format":"date-time","type":"string"},"application_open":{"format":"date-time","type":"string"},"application_review_started":{"type":"boolean"},"banner":{"type":["string","null"]},"created_at":{"format":"date-time","type":"string"},"decision_release":{"format":"date-time","type":["string","null"]},"description":{"type":["string","null"]},"early_application_close":{"format":"date-time","type":["string","null"]},"early_application_open":{"format":"date-time","type":["string","null"]},"end_time":{"format":"date-time","type":"string"},"id":{"type":"string"},"is_active":{"type":"boolean"},"location":{"type":["string","null"]},"location_url":{"type":["string","null"]},"max_attendees":{"format":"int32","type":["integer","null"]},"name":{"type":"string"},"rsvp_deadline":{"format":"date-time","type":["string","null"]},"start_time":{"format":"date-time","type":"string"},"updated_at":{"format":"date-time","type":"string"}},"required":["id","name","description","location","location_url","max_attendees","application_open","application_close","rsvp_deadline","decision_release","start_time","end_time","is_active","created_at","updated_at","banner","application_review_started","accept_early_applications","early_application_open","early_application_close"],"type":"object"},"HackerApplication":{"additionalProperties":false,"properties":{"application":{"contentEncoding":"base64","type":"string"},"createdAt":{"format":"date-time","type":"string"},"hackathonId":{"type":"string"},"savedAt":{"format":"date-time","type":"string"},"status":{"type":"string"},"submittedAt":{"format":"date-time","type":["string","null"]},"updatedAt":{"format":"date-time","type":"string"},"userId":{"type":"string"}},"required":["userId","status","application","createdAt","savedAt","updatedAt","submittedAt","hackathonId"],"type":"object"},"ListJoinRequestsByTeamAndStatusWithUserRow":{"additionalProperties":false,"properties":{"created_at":{"format":"date-time","type":"string"},"id":{"type":"string"},"processed_at":{"format":"date-time","type":["string","null"]},"processed_by_user_id":{"type":"string"},"request_message":{"type":["string","null"]},"status":{"type":"string"},"team_id":{"type":"string"},"updated_at":{"format":"date-time","type":"string"},"user_email":{"type":["string","null"]},"user_id":{"type":"string"},"user_image":{"type":["string","null"]},"user_name":{"type":"string"}},"required":["id","team_id","user_id","request_message","status","processed_by_user_id","processed_at","created_at","updated_at","user_email","user_name","user_image"],"type":"object"},"MemberWithUserInfo":{"additionalProperties":false,"properties":{"email":{"type":["string","null"]},"image":{"type":["string","null"]},"joinedAt":{"format":"date-time","type":"string"},"name":{"type":"string"},"userID":{"type":"string"}},"required":["userID","email","image","name","joinedAt"],"type":"object"},"OnboardingRequest":{"additionalProperties":false,"properties":{"name":{"type":"string"},"preferredEmail":{"type":"string"}},"required":["name","preferredEmail"],"type":"object"},"PublicHackathon":{"additionalProperties":false,"properties":{"acceptEarlyApplications":{"type":"boolean"},"applicationClose":{"format":"date-time","type":"string"},"applicationOpen":{"format":"date-time","type":"string"},"banner":{"type":["string","null"]},"description":{"type":["string","null"]},"earlyApplicationClose":{"format":"date-time","type":["string","null"]},"earlyApplicationOpen":{"format":"date-time","type":["string","null"]},"endTime":{"format":"date-time","type":"string"},"id":{"type":"string"},"location":{"type":["string","null"]},"locationUrl":{"type":["string","null"]},"name":{"type":"string"},"rsvpDeadline":{"format":"date-time","type":["string","null"]},"startTime":{"format":"date-time","type":"string"}},"required":["id","name","description","location","locationUrl","applicationOpen","applicationClose","acceptEarlyApplications","earlyApplicationOpen","earlyApplicationClose","rsvpDeadline","startTime","endTime","banner"],"type":"object"},"QueueConfirmationEmailRequest":{"additionalProperties":false,"properties":{"email":{"type":"string"},"firstName":{"type":"string"}},"required":["email","firstName"],"type":"object"},"QueueTextEmailRequest":{"additionalProperties":false,"properties":{"body":{"minLength":1,"type":"string"},"subject":{"minLength":1,"type":"string"},"to":{"items":{"type":"string"},"type":["array","null"]}},"required":["to","subject","body"],"type":"object"},"QueueWelcomeEmailRequest":{"additionalProperties":false,"properties":{"email":{"type":"string"},"firstName":{"type":"string"},"recipientId":{"type":"string"}},"required":["email","firstName","recipientId"],"type":"object"},"Redeemable":{"additionalProperties":false,"properties":{"amount":{"format":"int32","type":"integer"},"created_at":{"format":"date-time","type":"string"},"hackathon_id":{"type":"string"},"id":{"type":"string"},"max_user_amount":{"format":"int32","type":"integer"},"name":{"type":"string"},"updated_at":{"format":"date-time","type":"string"}},"required":["id","name","amount","max_user_amount","created_at","updated_at","hackathon_id"],"type":"object"},"ReviewRatings":{"additionalProperties":false,"properties":{"experienceRating":{"format":"int64","maxLength":5,"minLength":1,"type":"integer"},"passionRating":{"format":"int64","maxLength":5,"minLength":1,"type":"integer"}},"required":["passionRating","experienceRating"],"type":"object"},"ReviewerAssignment":{"additionalProperties":false,"properties":{"amount":{"format":"int64","type":["integer","null"]},"userID":{"type":"string"}},"required":["userID","amount"],"type":"object"},"SubmitInterestEmailRequest":{"additionalProperties":false,"properties":{"email":{"type":"string"},"source":{"type":["string","null"]}},"required":["email","source"],"type":"object"},"Team":{"additionalProperties":false,"properties":{"created_at":{"format":"date-time","type":"string"},"hackathon_id":{"type":"string"},"id":{"type":"string"},"name":{"type":"string"},"owner_id":{"type":"string"},"updated_at":{"format":"date-time","type":"string"}},"required":["id","name","owner_id","created_at","updated_at","hackathon_id"],"type":"object"},"TeamJoinRequest":{"additionalProperties":false,"properties":{"created_at":{"format":"date-time","type":"string"},"id":{"type":"string"},"processed_at":{"format":"date-time","type":["string","null"]},"processed_by_user_id":{"type":"string"},"request_message":{"type":["string","null"]},"status":{"type":"string"},"team_id":{"type":"string"},"updated_at":{"format":"date-time","type":"string"},"user_id":{"type":"string"}},"required":["id","team_id","user_id","request_message","status","processed_by_user_id","processed_at","created_at","updated_at"],"type":"object"},"TeamWithMembers":{"additionalProperties":false,"properties":{"id":{"type":"string"},"members":{"items":{"$ref":"#/components/schemas/MemberWithUserInfo"},"type":["array","null"]},"name":{"type":"string"},"ownerId":{"type":"string"}},"required":["id","ownerId","name","members"],"type":"object"},"UpdateEmailConsentRequest":{"additionalProperties":false,"properties":{"emailConsent":{"type":"boolean"}},"required":["emailConsent"],"type":"object"},"UpdateHackathonRequest":{"additionalProperties":false,"properties":{"applicationClose":{"format":"date-time","type":"string"},"applicationOpen":{"format":"date-time","type":"string"},"decisionRelease":{"format":"date-time","type":["string","null"]},"description":{"type":["string","null"]},"endTime":{"format":"date-time","type":"string"},"location":{"type":["string","null"]},"locationUrl":{"type":["string","null"]},"maxAttendees":{"format":"int32","type":["integer","null"]},"name":{"type":"string"},"rsvpDeadline":{"format":"date-time","type":["string","null"]},"startTime":{"format":"date-time","type":"string"}},"type":"object"},"UpdateRedeemableRequest":{"additionalProperties":false,"properties":{"maxUserAmount":{"format":"int64","type":"integer"},"name":{"type":"string"},"totalStock":{"format":"int64","type":"integer"}},"type":"object"},"UpdateRedemptionRequest":{"additionalProperties":false,"properties":{"newAmount":{"format":"int64","type":"integer"}},"type":"object"},"UpdateUserRequest":{"additionalProperties":false,"properties":{"name":{"type":"string"},"preferredEmail":{"type":"string"}},"required":["name","preferredEmail"],"type":"object"},"User":{"additionalProperties":false,"properties":{"checked_in_at":{"format":"date-time","type":["string","null"]},"created_at":{"format":"date-time","type":"string"},"email":{"type":["string","null"]},"email_consent":{"type":"boolean"},"email_verified":{"type":"boolean"},"id":{"type":"string"},"image":{"type":["string","null"]},"name":{"type":"string"},"onboarded":{"type":"boolean"},"preferred_email":{"type":["string","null"]},"rfid":{"type":["string","null"]},"role":{"type":"string"},"role_assigned_at":{"format":"date-time","type":["string","null"]},"updated_at":{"format":"date-time","type":"string"}},"required":["id","name","email","email_verified","onboarded","image","created_at","updated_at","preferred_email","email_consent","checked_in_at","rfid","role_assigned_at","role"],"type":"object"},"UserContext":{"additionalProperties":false,"properties":{"checkedInAt":{"format":"date-time","type":["string","null"]},"email":{"examples":["user@example.com"],"type":["string","null"]},"emailConsent":{"examples":[false],"type":"boolean"},"image":{"examples":["https://cdn.example.com/avatar.png"],"type":["string","null"]},"name":{"examples":["Jane Doe"],"type":"string"},"onboarded":{"examples":[true],"type":"boolean"},"preferredEmail":{"examples":["user.alt@example.com"],"type":["string","null"]},"rfid":{"type":["string","null"]},"role":{"enum":["admin","staff","attendee","applicant","visitor"],"type":"string"},"userId":{"examples":["550e8400-e29b-41d4-a716-446655440000"],"format":"uuid","type":"string"}},"required":["userId","email","preferredEmail","name","onboarded","image","role","emailConsent","rfid","checkedInAt"],"type":"object"}}},"info":{"title":"SwampHacks API","version":"1.0.0"},"openapi":"3.1.0","paths":{"/application":{"get":{"description":"Get the application of the current user","operationId":"get-application","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HackerApplication"}}},"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get Application","tags":["Application"]}},"/application/accept-acceptance":{"patch":{"description":"Accept an acceptance after being accepted. Sets event role to attendee, from applicant.","operationId":"accept-application-acceptance","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Accept Application Acceptance","tags":["Application"]}},"/application/assigned":{"get":{"description":"Returns assigned applications and their review progress for the authenticated reviewer","operationId":"get-assigned-applications","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/AssignedApplication"},"type":["array","null"]}}},"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get Assigned Applications","tags":["Application"]}},"/application/calculate-admissions":{"post":{"description":"Queues an admission calculation task to the BAT worker","operationId":"calculate-admissions-request","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Submit Admissions Calculation Request","tags":["Application"]}},"/application/join-waitlist":{"patch":{"description":"Adds a waitlist join time to application. Sets status to waitlisted","operationId":"join-waitlist","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Join Waitlist","tags":["Application"]}},"/application/release-decisions/{runId}":{"post":{"description":"Releases decisions that were calculated by the worker from a specific run id","operationId":"release-decisions","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"in":"path","name":"runId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Release Decisions","tags":["Application"]}},"/application/resume":{"get":{"description":"Returns a presigned S3 URL with GET permission for the user's specific object, which is their uploaded resume. The client can use this URL to download the object.","operationId":"get-download-resume-url","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get Resume Download URL","tags":["Application"]}},"/application/review/assign":{"post":{"description":"Assigns applications to reviewers for the application review process.","operationId":"assign-application-reviewers","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/ReviewerAssignment"},"type":["array","null"]}}},"required":true},"responses":{"200":{"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Assign Application Reviewers","tags":["Application"]}},"/application/review/reset":{"post":{"description":"Resets all application reviews, clearing any existing reviewer assignments.","operationId":"reset-application-reviews","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Reset Application Reviews","tags":["Application"]}},"/application/review/{applicantId}":{"post":{"description":"Handles ratings submissions from staff during the application review process","operationId":"submit-application-review","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"in":"path","name":"applicantId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReviewRatings"}}},"required":true},"responses":{"200":{"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Submit Application Review","tags":["Application"]}},"/application/review/{applicantId}/resume":{"get":{"description":"Returns a presigned S3 URL with GET permission for a specific user's resume as an object. The client can use this URL to download the object temporarily for application review.","operationId":"get-resume","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"in":"path","name":"applicantId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get Resume URL (for review process)","tags":["Application"]}},"/application/save":{"post":{"description":"Save user's progress on the application. File/Upload fields are not saved (eg. resumes).","operationId":"save-application","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{}}}},"responses":{"200":{"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Save Application","tags":["Application"]}},"/application/stats":{"get":{"description":"Aggregates applications by race, gender, age, majors, and schools","operationId":"get-application-statistics","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApplicationStatistics"}}},"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get Application Statistics","tags":["Application"]}},"/application/submit":{"post":{"description":"Submit the application","operationId":"submit-application","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Submit Application","tags":["Application"]}},"/application/transition-waitlisted-applications":{"patch":{"description":"Transitions all accepted users to waitlist, and accepts 50 from the waitlist. Sets application status from accepted to rejected.","operationId":"transition-waitlist","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Transition Waitlisted Applications","tags":["Application"]}},"/application/withdraw-acceptance":{"patch":{"description":"Withdraw an acceptance after being accepted to an event. Sets application status from accepted to rejected.","operationId":"withdraw-acceptance","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Withdraw Acceptance","tags":["Application"]}},"/application/withdraw-attendance":{"patch":{"description":"Withdraw attendance after accepting to go to the hackathon. Sets application status from accepted to withdrawn. Sets event role from attendee, back to applicant.","operationId":"withdraw-attendance","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Withdraw Attendance","tags":["Application"]}},"/auth/callback":{"get":{"description":"Handles the OAuth provider callback, validates state and nonce, and sets the session cookie.","operationId":"oauth-callback","parameters":[{"description":"OAuth authorization code","explode":false,"in":"query","name":"code","required":true,"schema":{"description":"OAuth authorization code","type":"string"}},{"description":"Base64 encoded OAuth state","explode":false,"in":"query","name":"state","required":true,"schema":{"description":"Base64 encoded OAuth state","type":"string"}},{"description":"Auth nonce cookie for CSRF protection","in":"cookie","name":"sh_auth_nonce","required":true,"schema":{"description":"Auth nonce cookie for CSRF protection","type":"string"}},{"description":"Client user agent","in":"header","name":"User-Agent","schema":{"description":"Client user agent","type":"string"}}],"responses":{"204":{"description":"No Content","headers":{"Location":{"schema":{"type":"string"}},"Set-Cookie":{"schema":{"type":"string"}}}},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"},"501":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Implemented"}},"summary":"OAuth Callback","tags":["Auth"]}},"/auth/logout":{"post":{"description":"Logs out the authenticated user by invalidating their session","operationId":"logout","responses":{"204":{"description":"No Content","headers":{"Set-Cookie":{"schema":{"type":"string"}}}},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Logout","tags":["Auth"]}},"/email/queue-confirmation-email":{"post":{"description":"Pushes a confirmation email request to the task queue","operationId":"queue-confirmation-email","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueueConfirmationEmailRequest"}}},"required":true},"responses":{"204":{"description":"No Content"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Queue Confirmation Email","tags":["Email"]}},"/email/queue-text-email":{"post":{"description":"Pushes a text email request to the task queue","operationId":"queue-text-email","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueueTextEmailRequest"}}},"required":true},"responses":{"204":{"description":"No Content"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Queue Text Email","tags":["Email"]}},"/email/queue-welcome-email":{"post":{"description":"Pushes a welcome email request to the task queue","operationId":"queue-welcome-email","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueueWelcomeEmailRequest"}}},"required":true},"responses":{"204":{"description":"No Content"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Queue Welcome Email","tags":["Email"]}},"/email/send-welcome-emails":{"post":{"description":"Send welcome emails to all attendees","operationId":"send-welcome-emails","responses":{"204":{"description":"No Content"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Send Welcome Emails","tags":["Email"]}},"/hackathon":{"get":{"description":"Returns public information of the hackathon","operationId":"get-hackathon","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicHackathon"}}},"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get Hackathon","tags":["Hackathon"]},"patch":{"description":"Updates the information of the hackathon","operationId":"update-hackathon","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateHackathonRequest"}}},"required":true},"responses":{"200":{"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Update Hackathon","tags":["Hackathon"]}},"/hackathon/attendees/count":{"get":{"description":"Returns the number of users who is attending the hackathon","operationId":"get-hackathon-attendees-count","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"format":"int64","type":"integer"}}},"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get Hackathon Attendees Count","tags":["Hackathon"]}},"/hackathon/attendees/discord":{"get":{"description":"Returns all users with a discord account that is also attending the hackathon","operationId":"get-hackathon-attendees-with-discord","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/GetAttendeesWithDiscordRow"},"type":["array","null"]}}},"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get Hackathon Attendees with Discord","tags":["Hackathon"]}},"/hackathon/attendees/userids":{"get":{"description":"Returns all users ids of users who are attending the hackathon","operationId":"get-hackathon-attendees-userids","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"type":"string"},"type":["array","null"]}}},"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get Hackathon Attendees User Ids","tags":["Hackathon"]}},"/hackathon/banner":{"delete":{"description":"Deletes the banner","operationId":"delete-banner","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"No Content"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Delete Banner","tags":["Hackathon"]},"post":{"description":"Uploads an image to be used as the banner for the hackathon","operationId":"upload-banner","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"multipart/form-data":{"encoding":{"image":{"contentType":"image/png, image/jpeg, image/jpg"}},"schema":{"properties":{"image":{"contentEncoding":"binary","contentMediaType":"application/octet-stream","format":"binary","type":"string"}},"required":["image"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"type":["string","null"]}}},"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Upload Banner","tags":["Hackathon"]}},"/hackathon/checkin":{"post":{"description":"Staff route for checking a user to an event. The user to check in must be an attendee and have never been checked in yet.","operationId":"check-in","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckInRequest"}}},"required":true},"responses":{"200":{"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Check In User","tags":["Hackathon"]}},"/hackathon/detailed":{"get":{"description":"Returns all information of the hackathon","operationId":"get-hackathon-for-staff","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Hackathon"}}},"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get Detailed Hackathon","tags":["Hackathon"]}},"/hackathon/interest":{"post":{"description":"Submits an email to interest/mailing list for the hackathon","operationId":"submit-interest-email","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmitInterestEmailRequest"}}},"required":true},"responses":{"200":{"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Submit Interest Email","tags":["Hackathon"]}},"/hackathon/staff":{"get":{"description":"Returns the users who are part of the current staff of the hackathon","operationId":"get-hackathon-staff","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/User"},"type":["array","null"]}}},"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get Hackathon Staff","tags":["Hackathon"]}},"/ping":{"get":{"description":"Health Check","operationId":"ping","responses":{"200":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"OK"},"default":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Error"}},"summary":"Ping","tags":["Misc"]}},"/redeemables":{"get":{"description":"Returns a list of all redeemable items","operationId":"get-redeemables","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/GetRedeemablesRow"},"type":["array","null"]}}},"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get Redeemables","tags":["Redeemables"]},"post":{"description":"Creates a new redeemable item","operationId":"create-redeemable","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateRedeemableRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Redeemable"}}},"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Create Redeemable","tags":["Redeemables"]}},"/redeemables/{redeemableId}":{"delete":{"description":"Deletes a redeemable by id","operationId":"delete-redeemable","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"in":"path","name":"redeemableId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Delete Redeemable","tags":["Redeemables"]},"patch":{"description":"Update specific fields (name, stock, max per user) of a redeemable","operationId":"update-redeemable","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"in":"path","name":"redeemableId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateRedeemableRequest"}}},"required":true},"responses":{"200":{"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Update Redeemable","tags":["Redeemables"]}},"/redeemables/{redeemableId}/users/{userID}":{"patch":{"description":"Updates a redemption created by the user.","operationId":"update-redemption","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"in":"path","name":"redeemableId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateRedemptionRequest"}}},"required":true},"responses":{"200":{"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Update Redemption","tags":["Redeemables"]},"post":{"description":"Redeems a redeemable by id. Creates a redemption record linking a specific user to a redeemable item","operationId":"redeem-redeemable","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"in":"path","name":"redeemableId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Redeem Redeemable","tags":["Redeemables"]}},"/teams":{"post":{"description":"Creates a new team and assigns the user as the owner. Returns the team.","operationId":"create-team","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateTeamRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Team"}}},"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"409":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Conflict"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Create Team","tags":["Team"]}},"/teams/me":{"get":{"description":"Returns the team information and the full list of team members for the currently authenticated user","operationId":"get-my-team","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeamWithMembers"}}},"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get My Team","tags":["Team"]}},"/teams/me/pending-joins":{"get":{"description":"Returns the current user's pending requests for teams.","operationId":"get-my-pending-join-requests","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/TeamJoinRequest"},"type":["array","null"]}}},"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get User's Pending Join Requests","tags":["Team"]}},"/teams/{requestId}/accept":{"post":{"description":"Accepts a pending team join request. Only the team owner can perform this action.","operationId":"accept-team-join-request","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"in":"path","name":"requestId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Accept Team Join Request","tags":["Team"]}},"/teams/{requestId}/reject":{"post":{"description":"Rejects a pending team join request. Only the team owner can perform this action.","operationId":"reject-team-join-request","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"in":"path","name":"requestId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Reject Team Join Request","tags":["Team"]}},"/teams/{teamId}":{"get":{"description":"Returns the team information and the full list of team members by team id","operationId":"get-team","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"in":"path","name":"teamId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeamWithMembers"}}},"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get Team","tags":["Team"]}},"/teams/{teamId}/join":{"post":{"description":"Requests to join a team or fails if user is already on a team.","operationId":"create-join-team-request","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"in":"path","name":"teamId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateJoinRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeamJoinRequest"}}},"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"409":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Conflict"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Request to Join Team","tags":["Team"]}},"/teams/{teamId}/kick/{memberId}":{"post":{"description":"Kicks a member from a team. Only the team owner can perform this action.","operationId":"kick-member-from-team","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"in":"path","name":"memberId","required":true,"schema":{"type":"string"}},{"in":"path","name":"teamId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Kick Team Member","tags":["Team"]}},"/teams/{teamId}/leave":{"post":{"description":"Leaves a team if the user is on the team.","operationId":"leave-team","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"in":"path","name":"teamId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Leave Team","tags":["Team"]}},"/teams/{teamId}/pending-joins":{"get":{"description":"Returns a team's pending join requests. This is only allowed for the team's owner.","operationId":"get-pending-join-team-requests","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"in":"path","name":"teamId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/ListJoinRequestsByTeamAndStatusWithUserRow"},"type":["array","null"]}}},"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"403":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Forbidden"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get Pending Join Requests for Team","tags":["Team"]}},"/users":{"get":{"description":"Get or search for users by name or email. If no search term is provided, returns all users with pagination.","operationId":"get-users","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"explode":false,"in":"query","name":"search","schema":{"type":"string"}},{"explode":false,"in":"query","name":"limit","schema":{"default":50,"format":"int64","type":"integer"}},{"explode":false,"in":"query","name":"offset","schema":{"default":0,"format":"int64","type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/User"},"type":["array","null"]}}},"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get Users","tags":["Users"]}},"/users/email/{email}":{"get":{"description":"Returns the user associated with the email","operationId":"get-user-by-email","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"in":"path","name":"email","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}},"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get User By Email","tags":["Users"]}},"/users/me":{"get":{"description":"Returns the authenticated user's profile","operationId":"get-me","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserContext"}}},"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get Me","tags":["Users"]},"patch":{"description":"Updates information of the authenticated user","operationId":"update-user","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateUserRequest"}}},"required":true},"responses":{"200":{"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Update User","tags":["Users"]}},"/users/me/email-consent":{"patch":{"description":"Updates the user's email consent setting","operationId":"update-email-consent","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateEmailConsentRequest"}}},"required":true},"responses":{"200":{"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Update Email Consent","tags":["Users"]}},"/users/me/onboarding":{"patch":{"description":"Allows the user to submit information such as name and preferred email, and complete the onboarding process","operationId":"onboard-user","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OnboardingRequest"}}},"required":true},"responses":{"200":{"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Onboard User","tags":["Users"]}},"/users/rfid/{rfid}":{"get":{"description":"Returns the user associated with the RFID","operationId":"get-user-by-rfid","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"in":"path","name":"rfid","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}},"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get User By RFID","tags":["Users"]}},"/users/roles/assign":{"post":{"description":"Assigns/modify a user's role","operationId":"assign-role","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssignRoleRequest"}}},"required":true},"responses":{"204":{"description":"No Content"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Assign Role","tags":["Users"]}},"/users/roles/batch-assign":{"post":{"description":"Batch assign/modify multiple users' roles","operationId":"batch-assign-roles","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssignRoleBatchRequest"}}},"required":true},"responses":{"204":{"description":"No Content"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Batch Assign Roles","tags":["Users"]}},"/users/roles/revoke/{userID}":{"post":{"description":"Remove a user's role","operationId":"revoke-role","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"in":"path","name":"userID","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"No Content"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Revoke Role","tags":["Users"]}},"/users/userid/{userID}":{"get":{"description":"Returns the user associated with the user id","operationId":"get-user-by-id","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"in":"path","name":"userID","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}},"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get User By Id","tags":["Users"]}}}} \ No newline at end of file diff --git a/apps/api/internal/api/api.go b/apps/api/internal/api/api.go index dc88253e..2dc339d7 100644 --- a/apps/api/internal/api/api.go +++ b/apps/api/internal/api/api.go @@ -19,7 +19,6 @@ import ( "github.com/swamphacks/core/apps/api/internal/database/repository" "github.com/swamphacks/core/apps/api/internal/domains/application" "github.com/swamphacks/core/apps/api/internal/domains/auth" - "github.com/swamphacks/core/apps/api/internal/domains/bat" "github.com/swamphacks/core/apps/api/internal/domains/email" "github.com/swamphacks/core/apps/api/internal/domains/hackathon" "github.com/swamphacks/core/apps/api/internal/domains/redeemables" @@ -80,7 +79,7 @@ func Run() { })) humaConfig := huma.DefaultConfig("SwampHacks API", "1.0.0") - humaConfig.DocsRenderer = huma.DocsRendererScalar + humaConfig.DocsRenderer = huma.DocsRendererSwaggerUI humaConfig.CreateHooks = nil // TODO: figure out a way to override the default schema name @@ -111,14 +110,13 @@ func Run() { accountRepo := repository.NewAccountRespository(db) sessionRepo := repository.NewSessionRepository(db) hackathonRepo := repository.NewHackathonRepository(db) - applicationRepo := repository.NewApplicationRepository(db) teamRepo := repository.NewTeamRespository(db) teamMemberRepo := repository.NewTeamMemberRespository(db) teamJoinRequestRepo := repository.NewTeamJoinRequestRepository(db) redeemablesRepo := repository.NewRedeemablesRepository(db) - batRunsRepo := repository.NewBatRunsRepository(db) eventInterestsRepo := repository.NewEventInterestsRepository(db) workshopRepo := repository.NewWorkshopsRepository(db) + emailCampaignRepo := repository.NewEmailCampaignRepository(db) mw := mw.NewMiddleware(userRepo, db, logger, config) @@ -139,9 +137,13 @@ func Run() { emailHandler := email.NewHandler(emailService, logger) email.RegisterRoutes(emailHandler, huma.NewGroup(api, "/email"), mw) - batService := bat.NewBatService(applicationRepo, hackathonRepo, userRepo, batRunsRepo, emailService, txm, taskQueueClient, nil, config, logger) - applicationService := application.NewService(applicationRepo, userRepo, hackathonRepo, txm, r2Client, &config.CoreBuckets, nil, emailService, batService, config, logger) - applicationHandler := application.NewHandler(applicationService, batService, config, logger) + emailCampaignService := email.NewEmailCampaignService(emailCampaignRepo, logger) + emailCampaignHandler := email.NewCampaignHandler(emailCampaignService, logger) + email.RegisterCampaignRoutes(emailCampaignHandler, huma.NewGroup(api, "/email"), mw) + + // batService := bat.NewBatService(applicationRepo, hackathonRepo, userRepo, batRunsRepo, emailService, txm, taskQueueClient, nil, config, logger) + applicationService := application.NewService(db, txm, r2Client, &config.CoreBuckets, nil, emailService, config, logger) + applicationHandler := application.NewHandler(applicationService, config, logger) application.RegisterRoutes(applicationHandler, huma.NewGroup(api, "/application"), mw) teamService := teams.NewService(teamRepo, teamMemberRepo, teamJoinRequestRepo, hackathonRepo, userRepo, txm, logger) diff --git a/apps/api/internal/api/middleware/auth.go b/apps/api/internal/api/middleware/auth.go index 1135eab5..f43032b9 100644 --- a/apps/api/internal/api/middleware/auth.go +++ b/apps/api/internal/api/middleware/auth.go @@ -65,6 +65,8 @@ type UserContext struct { Rfid *string `json:"rfid"` CheckedInAt *time.Time `json:"checkedInAt"` + + HasSeeNewApplicationStatus *bool `json:"hasSeenNewApplicationStatus"` } type SessionContext struct { @@ -191,16 +193,17 @@ func (m *AuthMiddleware) RequireAuth(next http.Handler) http.Handler { // TODO: I don't think we need UserContext here, just return sqlc.User directly userContext := UserContext{ - UserID: user.UserID, - Name: user.Name, - Email: user.Email, - PreferredEmail: user.PreferredEmail, - Image: user.Image, - Onboarded: user.Onboarded, - Role: user.Role, - EmailConsent: user.EmailConsent, - Rfid: user.Rfid, - CheckedInAt: user.CheckedInAt, + UserID: user.UserID, + Name: user.Name, + Email: user.Email, + PreferredEmail: user.PreferredEmail, + Image: user.Image, + Onboarded: user.Onboarded, + Role: user.Role, + EmailConsent: user.EmailConsent, + Rfid: user.Rfid, + CheckedInAt: user.CheckedInAt, + HasSeeNewApplicationStatus: user.HasSeenNewApplicationStatus, } sessionContext := SessionContext{ diff --git a/apps/api/internal/database/database.go b/apps/api/internal/database/database.go index afd8ccaf..95db3625 100644 --- a/apps/api/internal/database/database.go +++ b/apps/api/internal/database/database.go @@ -2,8 +2,11 @@ package database import ( "context" + "errors" "log" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" "github.com/jackc/pgx/v5/pgxpool" "github.com/swamphacks/core/apps/api/internal/database/sqlc" ) @@ -14,7 +17,16 @@ type DB struct { } func NewDB(connStr string) *DB { - pool, err := pgxpool.New(context.Background(), connStr) + poolConfig, err := pgxpool.ParseConfig(connStr) + if err != nil { + log.Fatal(err) + } + + poolConfig.AfterConnect = func(ctx context.Context, conn *pgx.Conn) error { + return registerCustomTypes(ctx, conn) + } + + pool, err := pgxpool.NewWithConfig(context.Background(), poolConfig) if err != nil { log.Fatal(err) } @@ -29,6 +41,43 @@ func NewDB(connStr string) *DB { return &db } +func (d *DB) NewTX(tx pgx.Tx) *DB { + txDB := DB{ + Pool: d.Pool, + Query: sqlc.New(tx), + } + + return &txDB +} + func (d *DB) Close() { d.Pool.Close() } + +func registerCustomTypes(ctx context.Context, conn *pgx.Conn) error { + typeNames := []string{ + "email_campaign_format", + "email_campaign_status", + "email_recipient_type", + "_email_recipient_type", + } + + for _, typeName := range typeNames { + dataType, err := conn.LoadType(ctx, typeName) + if isUndefinedType(err) { + continue + } + if err != nil { + return err + } + + conn.TypeMap().RegisterType(dataType) + } + + return nil +} + +func isUndefinedType(err error) bool { + var pgErr *pgconn.PgError + return errors.As(err, &pgErr) && pgErr.Code == "42704" +} diff --git a/apps/api/internal/database/migrations/20260502234849_has_seen_application_status.sql b/apps/api/internal/database/migrations/20260502234849_has_seen_application_status.sql new file mode 100644 index 00000000..ea9bb47b --- /dev/null +++ b/apps/api/internal/database/migrations/20260502234849_has_seen_application_status.sql @@ -0,0 +1,5 @@ +-- +goose Up +alter table users add has_seen_new_application_status boolean; + +-- +goose Down +alter table users drop column has_seen_new_application_status; diff --git a/apps/api/internal/database/migrations/20260605151151_add_confirmed_type_to_application_status.sql b/apps/api/internal/database/migrations/20260605151151_add_confirmed_type_to_application_status.sql new file mode 100644 index 00000000..49600cf3 --- /dev/null +++ b/apps/api/internal/database/migrations/20260605151151_add_confirmed_type_to_application_status.sql @@ -0,0 +1,5 @@ +-- +goose Up +ALTER TYPE application_status +ADD VALUE IF NOT EXISTS 'confirmed'; + +-- +goose Down diff --git a/apps/api/internal/database/migrations/20260618145942_applications_add_id_column.sql b/apps/api/internal/database/migrations/20260618145942_applications_add_id_column.sql new file mode 100644 index 00000000..98999f79 --- /dev/null +++ b/apps/api/internal/database/migrations/20260618145942_applications_add_id_column.sql @@ -0,0 +1,21 @@ +-- +goose Up +alter table applications drop column if exists waitlist_join_time; + +alter table applications add id uuid not null default gen_random_uuid(); + +alter table applications drop constraint applications_pkey; + +alter table applications add primary key (id); + +alter table applications add constraint one_application_per_user unique (user_id, hackathon_id); + +-- +goose Down +alter table applications add waitlist_join_time timestamptz; + +alter table applications drop constraint one_application_per_user; + +alter table applications drop constraint applications_pkey; + +alter table applications drop column if exists id; + +alter table applications add primary key (user_id); \ No newline at end of file diff --git a/apps/api/internal/database/migrations/20260618162831_application_reviews_and_decision_requests.sql b/apps/api/internal/database/migrations/20260618162831_application_reviews_and_decision_requests.sql new file mode 100644 index 00000000..e62c1b3f --- /dev/null +++ b/apps/api/internal/database/migrations/20260618162831_application_reviews_and_decision_requests.sql @@ -0,0 +1,66 @@ +-- +goose Up +alter table applications drop column if exists experience_rating; +alter table applications drop column if exists passion_rating; +alter table applications drop column if exists assigned_reviewer_id; + +create table application_reviews ( + id uuid default gen_random_uuid() not null primary key, + + application_id uuid not null references applications(id) on delete cascade, + reviewer_id uuid not null references users(id) on delete cascade, + + experience_rating integer, + passion_rating integer, + notes text, + + updated_by uuid references users(id) on delete set null, + + created_at timestamptz not null default now(), + updated_at timestamptz not null default now(), + + UNIQUE(application_id, reviewer_id) +); + +create type application_auto_decision_type as enum ( + 'auto_accept', + 'auto_reject' +); + +create table application_auto_decision_requests ( + id uuid default gen_random_uuid() not null primary key, + + application_id uuid not null references applications(id) on delete cascade, + reviewer_id uuid not null references users(id) on delete cascade, + + requested_decision application_auto_decision_type not null, + justification text, + approved boolean, + decided_by uuid references users(id) on delete set null, + + updated_at timestamptz not null default now(), + created_at timestamptz not null default now(), + + UNIQUE(application_id, reviewer_id) +); + +create trigger application_auto_decision_requests_updated_at +before update on application_auto_decision_requests +for each row +execute function update_modified_column(); + +-- +goose Down +drop table application_auto_decision_requests; +drop type application_auto_decision_type; +drop trigger if exists application_auto_decision_requests_updated_at + on application_auto_decision_requests; + +drop table application_reviews; + +alter table applications + add column experience_rating integer; + +alter table applications + add column passion_rating integer; + +alter table applications + add column assigned_reviewer_id uuid references users on delete set null; \ No newline at end of file diff --git a/apps/api/internal/database/queries/application_reviews.sql b/apps/api/internal/database/queries/application_reviews.sql new file mode 100644 index 00000000..dfaead51 --- /dev/null +++ b/apps/api/internal/database/queries/application_reviews.sql @@ -0,0 +1,147 @@ +-- name: AssignReviewerToApplications :exec +INSERT INTO application_reviews ( + application_id, + reviewer_id +) +SELECT applications.id, @reviewer_id::uuid FROM applications +WHERE applications.id = ANY(@application_ids::uuid[]) +ON CONFLICT DO NOTHING; + +-- name: ListReviewsByReviewerId :many +SELECT + ar.*, + applications.user_id +FROM application_reviews ar +JOIN applications ON applications.id = ar.application_id +WHERE reviewer_id = @reviewer_id +ORDER BY application_id ASC; + +-- name: ListApplicationReviewersById :many +SELECT reviewer_id FROM application_reviews +WHERE application_id = @application_id; + +-- name: ListReviewersAndProgress :many +SELECT + reviewer.id, + reviewer.name, + reviewer.image, + COUNT(*) AS total_assigned, + COUNT(*) FILTER ( + WHERE ar.experience_rating IS NOT NULL AND ar.passion_rating IS NOT NULL + ) AS completed_count +FROM application_reviews AS ar +LEFT JOIN users AS reviewer + ON reviewer.id = ar.reviewer_id +GROUP BY + reviewer.id; + +-- name: GetReviewById :one +SELECT + ar.*, + aadr.requested_decision, + aadr.id AS decision_request_id, + aadr.justification AS decision_justification, + aadr.approved AS decision_approved, + aadr.decided_by AS decision_decided_by, + aadr.created_at AS decision_request_created_at, + applications.user_id, + applications.application +FROM application_reviews AS ar +JOIN applications ON applications.id = ar.application_id +LEFT JOIN application_auto_decision_requests AS aadr ON aadr.application_id = ar.application_id +WHERE ar.id = @review_id; + +-- name: UpdateApplicationReview :exec +UPDATE application_reviews +SET + experience_rating = CASE WHEN @experience_rating_do_update::boolean THEN @experience_rating ELSE experience_rating END, + passion_rating = CASE WHEN @passion_rating_do_update::boolean THEN @passion_rating ELSE passion_rating END, + notes = CASE WHEN @notes_do_update::boolean THEN @notes ELSE notes END, + updated_by = CASE WHEN @updated_by_do_update::boolean THEN @updated_by ELSE updated_by END, + updated_at = NOW() +WHERE + id = @id AND + reviewer_id = @reviewer_id; + +-- name: DeleteAllApplicationReviews :exec +DELETE FROM application_reviews; + +-- name: RequestAutoDecision :one +INSERT INTO application_auto_decision_requests (application_id, reviewer_id, requested_decision, justification, approved, decided_by) +VALUES (@application_id, @reviewer_id, @requested_decision, @justification, @approved, @decided_by) RETURNING *; + +-- name: GetAutoDecisionRequestsCount :one +SELECT COUNT(*) FROM application_auto_decision_requests; + +-- name: SearchAutoDecisionRequests :many +SELECT + aadr.*, + reviewer.name AS reviewer_name, + reviewer.image AS reviewer_image, + approver.id AS approver_id, + approver.name AS approver_name, + approver.image AS approver_image, + applicant.id AS user_id, + applicant.name AS user_name, + applicant.image AS user_image +FROM application_auto_decision_requests AS aadr +JOIN users AS reviewer + ON reviewer.id = aadr.reviewer_id +JOIN applications + ON applications.id = aadr.application_id +JOIN users AS applicant + ON applicant.id = applications.user_id +LEFT JOIN users AS approver + ON approver.id = aadr.decided_by +WHERE (LOWER(reviewer.name) LIKE LOWER('%' || COALESCE(sqlc.arg('search'), '') || '%') + OR LOWER(applicant.name) LIKE LOWER('%' || COALESCE(sqlc.arg('search'), '') || '%')) + AND ( + sqlc.arg('approved')::text = 'all' + OR (sqlc.arg('approved')::text = 'pending' AND aadr.approved IS NULL) + OR (sqlc.arg('approved')::text = 'approved' AND aadr.approved = true) + OR (sqlc.arg('approved')::text = 'denied' AND aadr.approved = false) + ) + AND ( + sqlc.narg('decision')::application_auto_decision_type IS NULL + OR aadr.requested_decision = sqlc.narg('decision')::application_auto_decision_type + ) +ORDER BY aadr.created_at DESC +LIMIT sqlc.arg('limit') OFFSET sqlc.arg('offset'); + +-- name: ListAutoDecisionRequests :many +SELECT + aadr.*, + reviewer.name AS reviewer_name, + reviewer.image AS reviewer_image, + approver.id AS approver_id, + approver.name AS approver_name, + approver.image AS approver_image, + applicant.id AS user_id, + applicant.name AS user_name, + applicant.image AS user_image +FROM application_auto_decision_requests AS aadr +JOIN users AS reviewer + ON reviewer.id = aadr.reviewer_id +JOIN applications + ON applications.id = aadr.application_id +JOIN users AS applicant + ON applicant.id = applications.user_id +LEFT JOIN users AS approver + ON approver.id = aadr.decided_by +ORDER BY aadr.created_at DESC; + +-- name: DeleteAutoDecisionRequest :exec +DELETE FROM application_auto_decision_requests +WHERE id = @id AND reviewer_id = @reviewer_id; + +-- name: UpdateAutoDecisionRequest :exec +UPDATE application_auto_decision_requests +SET + requested_decision = CASE WHEN @requested_decision_do_update::boolean AND @requested_decision <> '' THEN @requested_decision::application_auto_decision_type ELSE requested_decision END, + justification = CASE WHEN @justification_do_update::boolean THEN @justification ELSE justification END, + approved = CASE WHEN @approved_do_update::boolean THEN @approved ELSE approved END, + decided_by = CASE WHEN @approved_by_do_update::boolean THEN @decided_by ELSE decided_by END +WHERE id = @id AND reviewer_id = @reviewer_id; + +-- name: DeleteAllAutoDecisionRequests :exec +DELETE FROM application_auto_decision_requests; \ No newline at end of file diff --git a/apps/api/internal/database/queries/stats.sql b/apps/api/internal/database/queries/application_stats.sql similarity index 91% rename from apps/api/internal/database/queries/stats.sql rename to apps/api/internal/database/queries/application_stats.sql index 8f1475ec..c6977a2f 100644 --- a/apps/api/internal/database/queries/stats.sql +++ b/apps/api/internal/database/queries/application_stats.sql @@ -1,6 +1,6 @@ -- Queries used for statistics, mainly used by overview dashboards etc --- name: GetApplicationGenderSplit :one +-- name: GetSubmittedApplicationGenders :one SELECT COUNT(*) FILTER (WHERE application->>'gender' = 'man') AS male, COUNT(*) FILTER (WHERE application->>'gender' = 'woman') AS female, @@ -9,7 +9,7 @@ SELECT FROM applications WHERE status <> 'started' AND status IS NOT NULL; --- name: GetApplicationAgeSplit :one +-- name: GetSubmittedApplicationAges :one SELECT COUNT(*) FILTER (WHERE (application->>'age')::int < 18) AS underage, COUNT(*) FILTER (WHERE (application->>'age')::int = 18) AS age_18, @@ -21,7 +21,7 @@ SELECT FROM applications WHERE status <> 'started' AND status IS NOT NULL; --- name: GetApplicationRaceSplit :many +-- name: GetSubmittedApplicationRaces :many SELECT CASE WHEN application->>'race' IS NOT NULL AND application->>'race' <> '' THEN application->>'race' @@ -39,7 +39,7 @@ GROUP BY END ORDER BY count DESC; --- name: GetApplicationSchoolSplit :many +-- name: GetSubmittedApplicationSchools :many SELECT (application->>'school')::text AS school, COUNT(*) AS count @@ -48,7 +48,7 @@ WHERE status <> 'started' AND status IS NOT NULL GROUP BY (application->>'school')::text ORDER BY count DESC; --- name: GetApplicationMajorSplit :many +-- name: GetSubmittedApplicationMajors :many SELECT trim(major) AS major, COUNT(*) AS count @@ -58,7 +58,7 @@ WHERE status <> 'started' AND status IS NOT NULL GROUP BY trim(major) ORDER BY count DESC; --- name: GetApplicationStatusSplit :one +-- name: GetApplicationStatuses :one SELECT COUNT(*) FILTER (WHERE status = 'started') AS started, COUNT(*) FILTER (WHERE status = 'submitted') AS submitted, diff --git a/apps/api/internal/database/queries/applications.sql b/apps/api/internal/database/queries/applications.sql index d2b05256..a5f9f3ec 100644 --- a/apps/api/internal/database/queries/applications.sql +++ b/apps/api/internal/database/queries/applications.sql @@ -1,41 +1,96 @@ -- name: CreateApplication :one -INSERT INTO applications (user_id, hackathon_id, is_early) VALUES ($1, $2, $3) RETURNING *; +INSERT INTO applications (user_id, hackathon_id, is_early) VALUES (@user_id, @hackathon_id, @is_early) RETURNING *; + +-- name: GetApplicationById :one +SELECT * FROM applications WHERE id = @id; -- name: GetApplicationByUserId :one -SELECT * FROM applications WHERE user_id = $1; +SELECT * FROM applications WHERE user_id = @user_id; + +-- name: UpdateApplicationById :exec +UPDATE applications +SET + status = CASE WHEN @status_do_update::boolean THEN @status::application_status ELSE status END, + application = CASE WHEN @application_do_update::boolean THEN @application::JSONB ELSE application END, + submitted_at = CASE WHEN @submitted_at_do_update::boolean THEN @submitted_at::timestamptz ELSE submitted_at END, + saved_at = CASE WHEN @saved_at_do_update::boolean THEN @saved_at::timestamptz ELSE saved_at END, + is_early = CASE WHEN @is_early_do_update::boolean THEN @is_early::boolean ELSE is_early END +WHERE + id = @id; --- name: UpdateApplication :exec +-- name: UpdateApplicationByUserId :exec UPDATE applications SET status = CASE WHEN @status_do_update::boolean THEN @status::application_status ELSE status END, application = CASE WHEN @application_do_update::boolean THEN @application::JSONB ELSE application END, submitted_at = CASE WHEN @submitted_at_do_update::boolean THEN @submitted_at::timestamptz ELSE submitted_at END, saved_at = CASE WHEN @saved_at_do_update::boolean THEN @saved_at::timestamptz ELSE saved_at END, - assigned_reviewer_id = CASE WHEN @assigned_reviewer_id_do_update::boolean THEN @assigned_reviewer_id::UUID ELSE assigned_reviewer_id END, - experience_rating = CASE WHEN @experience_rating_do_update::boolean THEN @experience_rating::INT ELSE experience_rating END, - passion_rating = CASE WHEN @passion_rating_do_update::boolean THEN @passion_rating::INT ELSE passion_rating END, is_early = CASE WHEN @is_early_do_update::boolean THEN @is_early::boolean ELSE is_early END WHERE user_id = @user_id; --- name: DeleteApplication :exec -DELETE FROM applications WHERE user_id = $1; +-- name: DeleteApplicationById :exec +DELETE FROM applications WHERE id = @id; + +-- name: SearchApplicationsWithUserInfo :many +SELECT + a.id, + a.user_id, + a.status, + a.created_at, + a.submitted_at, + a.application, + a.is_early, + u.name, + u.image, + u.email +FROM applications a +JOIN users u ON u.id = a.user_id +WHERE (LOWER(u.name) LIKE LOWER('%' || COALESCE(sqlc.arg('search'), '') || '%') + OR LOWER(u.email) LIKE LOWER('%' || COALESCE(sqlc.arg('search'), '') || '%')) + AND a.hackathon_id = @hackathon_id +ORDER BY a.created_at DESC +LIMIT sqlc.arg('limit') OFFSET sqlc.arg('offset'); + +-- name: GetExtendedApplicationById :one +SELECT + a.*, + ar.id AS review_id, + ar.experience_rating, + ar.passion_rating, + ar.notes, + ar.updated_at AS review_updated_at, + ar.updated_by AS review_updated_by, + reviewer.id AS reviewer_id, + reviewer.name AS reviewer_name, + reviewer.image AS reviewer_image, + applicant.name AS user_name, + applicant.image AS user_image, + applicant.email AS user_email, + aadr.requested_decision, + aadr.id AS auto_decision_request_id, + aadr.justification AS decision_justification, + aadr.approved AS decision_approved, + aadr.decided_by, + aadr.created_at AS decision_request_created_at +FROM applications a +JOIN users AS applicant ON applicant.id = a.user_id +LEFT JOIN application_reviews AS ar ON ar.application_id = a.id +LEFT JOIN users AS reviewer ON reviewer.id = ar.reviewer_id +LEFT JOIN application_auto_decision_requests as aadr ON aadr.application_id = a.id +WHERE a.id = @id; --- An application is considered "available" if the application has a status of submitted and has not been reviewed yet. --- For optimization purposes, we only select the application IDs. +-- name: GetApplicationsCount :one +SELECT COUNT(*) FROM applications WHERE hackathon_id = @hackathon_id; --- name: ListAvailableApplications :many -SELECT user_id FROM applications -WHERE status = 'submitted' - AND experience_rating IS NULL - AND passion_rating IS NULL -ORDER BY - user_id ASC; +-- name: ListUnderReviewApplicationIds :many +SELECT id FROM applications +WHERE status = 'under_review' AND hackathon_id = @hackathon_id +ORDER BY id ASC; --- name: ListAdmissionCandidates :many -SELECT a.user_id, - a.passion_rating, - a.experience_rating, +-- name: ListApplicationsUnderReviewWithTeamIds :many +SELECT + a.user_id, a.application, t.id as team_id FROM applications a @@ -43,48 +98,25 @@ LEFT JOIN team_members tm ON tm.user_id = a.user_id LEFT JOIN teams t ON t.id = tm.team_id -WHERE a.status = 'under_review' - AND a.passion_rating IS NOT NULL - AND a.experience_rating IS NOT NULL; +WHERE a.status = 'under_review'; --- name: AssignApplicationsToReviewer :exec -UPDATE applications -SET assigned_reviewer_id = @reviewer_id::uuid, - status = 'under_review' -WHERE user_id = ANY(@application_ids::uuid[]); - --- name: ResetApplicationReviews :exec -UPDATE applications -SET assigned_reviewer_id = NULL, - status = 'submitted', - experience_rating = NULL, - passion_rating = NULL -WHERE status NOT IN ('submitted', 'started'); - --- name: ListApplicationByReviewer :many -SELECT user_id, passion_rating, experience_rating FROM applications -WHERE assigned_reviewer_id = $1 - AND status IN ('under_review') -ORDER BY user_id ASC; - --- name: ListNonReviewedApplications :many -SELECT user_id -FROM applications -WHERE status = 'under_review' - AND (passion_rating IS NULL OR experience_rating IS NULL); - --- name: JoinWaitlist :exec +-- name: WaitlistApplicationById :exec UPDATE applications SET waitlist_join_time = COALESCE(waitlist_join_time, NOW()), status = 'waitlisted' -WHERE user_id = $1; +WHERE id = @id; --- name: UpdateApplicationStatus :exec +-- name: UpdateApplicationsStatusByIds :exec UPDATE applications SET status = @status::application_status -WHERE user_id = ANY(@user_ids::uuid[]); +WHERE id = ANY(@ids::uuid[]); + +-- name: MarkSubmittedApplicationsAsUnderReview :exec +UPDATE applications +SET status = 'under_review' +WHERE status = 'submitted'; --- name: TransitionAcceptedApplicationsToWaitlist :exec +-- name: WaitlistAcceptedApplications :exec UPDATE applications SET waitlist_join_time = COALESCE(waitlist_join_time, NOW()), status = 'waitlisted' @@ -94,7 +126,7 @@ WHERE status = 'accepted' WHERE role = 'applicant' ); --- name: TransitionWaitlistedApplicationsToAccepted :many +-- name: AcceptWaitlistedApplications :many UPDATE applications SET waitlist_join_time = NULL, status = 'accepted' @@ -106,3 +138,7 @@ WHERE user_id IN ( ) RETURNING user_id; +-- name: ResetApplicationsToSubmitted :exec +UPDATE applications +SET status = 'submitted' +WHERE status = 'under_review'; \ No newline at end of file diff --git a/apps/api/internal/database/queries/email_campaigns.sql b/apps/api/internal/database/queries/email_campaigns.sql index 5619c5cf..8d2fa02f 100644 --- a/apps/api/internal/database/queries/email_campaigns.sql +++ b/apps/api/internal/database/queries/email_campaigns.sql @@ -18,7 +18,7 @@ INSERT INTO email_campaigns ( @subject, @body, @format::email_campaign_format, - @recipient_types::email_recipient_type[], + sqlc.arg(recipient_types)::text[]::email_recipient_type[], sqlc.narg(scheduled_at), sqlc.narg(created_by_user_id), sqlc.narg(updated_by_user_id) @@ -61,11 +61,11 @@ SET ELSE body END, format = CASE WHEN @format_do_update::boolean - THEN @format::email_campaign_format + THEN sqlc.narg(format)::email_campaign_format ELSE format END, recipient_types = CASE WHEN @recipient_types_do_update::boolean - THEN @recipient_types::email_recipient_type[] + THEN sqlc.arg(recipient_types)::text[]::email_recipient_type[] ELSE recipient_types END, scheduled_at = CASE WHEN @scheduled_at_do_update::boolean @@ -102,4 +102,4 @@ SET ELSE updated_by_user_id END WHERE id = @id::uuid AND hackathon_id = @hackathon_id -RETURNING *; \ No newline at end of file +RETURNING *; diff --git a/apps/api/internal/database/queries/hackathons.sql b/apps/api/internal/database/queries/hackathons.sql index fdb115dd..f1409221 100644 --- a/apps/api/internal/database/queries/hackathons.sql +++ b/apps/api/internal/database/queries/hackathons.sql @@ -39,7 +39,8 @@ SET start_time = CASE WHEN @start_time_do_update::boolean THEN @start_time ELSE start_time END, end_time = CASE WHEN @end_time_do_update::boolean THEN @end_time ELSE end_time END, banner = CASE WHEN @banner_do_update::boolean THEN @banner ELSE banner END, - application_review_started = CASE WHEN @application_review_started_do_update::boolean THEN @application_review_started ELSE application_review_started END + application_review_started = CASE WHEN @application_review_started_do_update::boolean THEN @application_review_started ELSE application_review_started END, + updated_at = NOW() WHERE is_active = true RETURNING *; diff --git a/apps/api/internal/database/queries/sessions.sql b/apps/api/internal/database/queries/sessions.sql index a0fe6132..968a731b 100644 --- a/apps/api/internal/database/queries/sessions.sql +++ b/apps/api/internal/database/queries/sessions.sql @@ -21,7 +21,10 @@ DELETE FROM sessions WHERE expires_at < NOW(); -- name: GetActiveSessionUserInfo :one -SELECT u.id AS user_id, u.name, u.email, u.preferred_email, u.onboarded, u.image, u.role, u.email_consent, u.checked_in_at, u.rfid, s.last_used_at +SELECT u.id AS user_id, u.name, u.email, u.preferred_email, + u.onboarded, u.image, u.role, u.email_consent, + u.checked_in_at, u.rfid, u.has_seen_new_application_status, + s.last_used_at FROM sessions s JOIN users u ON s.user_id = u.id WHERE s.id = $1 diff --git a/apps/api/internal/database/queries/users.sql b/apps/api/internal/database/queries/users.sql index e1bf0a8f..d545d058 100644 --- a/apps/api/internal/database/queries/users.sql +++ b/apps/api/internal/database/queries/users.sql @@ -40,8 +40,9 @@ SET email_consent = CASE WHEN @email_consent_do_update::boolean THEN @email_consent ELSE email_consent END, checked_in_at = CASE WHEN @checked_in_at_do_update::boolean THEN @checked_in_at ELSE checked_in_at END, rfid = CASE WHEN @rfid_do_update::boolean THEN @rfid ELSE rfid END, - role = CASE WHEN @role_do_update::boolean THEN @role ELSE role END, - role_assigned_at = CASE WHEN @role_do_update::boolean THEN NOW() ELSE role_assigned_at END, + -- role = CASE WHEN @role_do_update::boolean THEN @role ELSE role END, + -- role_assigned_at = CASE WHEN @role_do_update::boolean THEN NOW() ELSE role_assigned_at END, + has_seen_new_application_status = CASE WHEN @has_seen_new_application_status_do_update::boolean THEN @has_seen_new_application_status ELSE has_seen_new_application_status END, updated_at = NOW() WHERE id = @id::uuid; @@ -82,4 +83,4 @@ WHERE id = @user_id::uuid; -- name: UpdateRFID :exec UPDATE users SET rfid = @rfid -WHERE id = @user_id::uuid; \ No newline at end of file +WHERE id = @user_id::uuid; diff --git a/apps/api/internal/database/repository/application.go b/apps/api/internal/database/repository/application.go deleted file mode 100644 index 472c7942..00000000 --- a/apps/api/internal/database/repository/application.go +++ /dev/null @@ -1,137 +0,0 @@ -package repository - -import ( - "context" - "errors" - - "github.com/google/uuid" - "github.com/jackc/pgx/v5" - "github.com/swamphacks/core/apps/api/internal/database" - "github.com/swamphacks/core/apps/api/internal/database/sqlc" -) - -type ApplicationRepository struct { - db *database.DB -} - -func NewApplicationRepository(db *database.DB) *ApplicationRepository { - return &ApplicationRepository{ - db: db, - } -} - -func (r *ApplicationRepository) NewTx(tx pgx.Tx) *ApplicationRepository { - txDB := &database.DB{ - Pool: r.db.Pool, - Query: sqlc.New(tx), - } - - return &ApplicationRepository{ - db: txDB, - } -} - -func (r *ApplicationRepository) CreateApplication(ctx context.Context, params sqlc.CreateApplicationParams) (*sqlc.Application, error) { - application, err := r.db.Query.CreateApplication(ctx, params) - - if err != nil { - return nil, err - } - - return &application, nil -} - -func (r *ApplicationRepository) UpdateApplication(ctx context.Context, params sqlc.UpdateApplicationParams) error { - return r.db.Query.UpdateApplication(ctx, params) -} - -func (r *ApplicationRepository) GetApplicationByUserId(ctx context.Context, userID uuid.UUID) (*sqlc.Application, error) { - application, err := r.db.Query.GetApplicationByUserId(ctx, userID) - - if err != nil { - if errors.Is(err, pgx.ErrNoRows) { - return nil, database.ErrApplicationNotFound - } - - return nil, err - } - - return &application, nil -} - -func (r *ApplicationRepository) UpdateApplicationsStatuses(ctx context.Context, params sqlc.UpdateApplicationStatusParams) error { - return r.db.Query.UpdateApplicationStatus(ctx, params) -} - -// List all candidates considered for admission. -// This queries for all applications who are 'under_review' and have their rating fields filled out. -// It also LEFT JOINs in their team id (if they have one) for further grouping based on teams. -func (r *ApplicationRepository) ListAdmissionCandidates(ctx context.Context) ([]sqlc.ListAdmissionCandidatesRow, error) { - return r.db.Query.ListAdmissionCandidates(ctx) -} - -func (r *ApplicationRepository) ListAvailableApplications(ctx context.Context) ([]uuid.UUID, error) { - return r.db.Query.ListAvailableApplications(ctx) -} - -func (r *ApplicationRepository) AssignApplicationToReview(ctx context.Context, params sqlc.AssignApplicationsToReviewerParams) error { - return r.db.Query.AssignApplicationsToReviewer(ctx, params) -} - -func (r *ApplicationRepository) ListApplicationByReviewer(ctx context.Context, reviewerID uuid.UUID) ([]sqlc.ListApplicationByReviewerRow, error) { - return r.db.Query.ListApplicationByReviewer(ctx, &reviewerID) -} - -func (r *ApplicationRepository) ResetApplicationReviews(ctx context.Context) error { - return r.db.Query.ResetApplicationReviews(ctx) -} - -func (r *ApplicationRepository) GetSubmittedApplicationGenders(ctx context.Context) (sqlc.GetApplicationGenderSplitRow, error) { - return r.db.Query.GetApplicationGenderSplit(ctx) -} - -func (r *ApplicationRepository) GetSubmittedApplicationRaces(ctx context.Context) ([]sqlc.GetApplicationRaceSplitRow, error) { - return r.db.Query.GetApplicationRaceSplit(ctx) -} - -func (r *ApplicationRepository) GetSubmittedApplicationAges(ctx context.Context) (sqlc.GetApplicationAgeSplitRow, error) { - return r.db.Query.GetApplicationAgeSplit(ctx) -} - -func (r *ApplicationRepository) GetSubmittedApplicationMajors(ctx context.Context) ([]sqlc.GetApplicationMajorSplitRow, error) { - return r.db.Query.GetApplicationMajorSplit(ctx) -} - -func (r *ApplicationRepository) GetSubmittedApplicationSchools(ctx context.Context) ([]sqlc.GetApplicationSchoolSplitRow, error) { - return r.db.Query.GetApplicationSchoolSplit(ctx) -} - -func (r *ApplicationRepository) GetApplicationStatuses(ctx context.Context) (sqlc.GetApplicationStatusSplitRow, error) { - return r.db.Query.GetApplicationStatusSplit(ctx) -} - -func (r *ApplicationRepository) GetNonReviewedApplications(ctx context.Context) ([]uuid.UUID, error) { - return r.db.Query.ListNonReviewedApplications(ctx) -} - -func (r *ApplicationRepository) GetSubmissionTimes(ctx context.Context) ([]sqlc.GetSubmissionTimesRow, error) { - return r.db.Query.GetSubmissionTimes(ctx) -} - -func (r *ApplicationRepository) JoinWaitlist(ctx context.Context, userID uuid.UUID) error { - return r.db.Query.JoinWaitlist(ctx, userID) -} - -func (r *ApplicationRepository) TransitionAcceptedApplicationsToWaitlist(ctx context.Context) error { - return r.db.Query.TransitionAcceptedApplicationsToWaitlist(ctx) -} - -func (r *ApplicationRepository) TransitionWaitlistedApplicationsToAccepted(ctx context.Context, acceptanceCount int32) ([]uuid.UUID, error) { - return r.db.Query.TransitionWaitlistedApplicationsToAccepted(ctx, acceptanceCount) -} - -func (r *ApplicationRepository) GetAttendeeCount(ctx context.Context) (uint32, error) { - amount, err := r.db.Query.GetAttendeeCount(ctx) - - return uint32(amount), err -} diff --git a/apps/api/internal/database/repository/email_campaigns.go b/apps/api/internal/database/repository/email_campaigns.go new file mode 100644 index 00000000..055f28c3 --- /dev/null +++ b/apps/api/internal/database/repository/email_campaigns.go @@ -0,0 +1,90 @@ +package repository + +import ( + "context" + "errors" + + "github.com/jackc/pgx/v5" + "github.com/swamphacks/core/apps/api/internal/database" + "github.com/swamphacks/core/apps/api/internal/database/sqlc" +) + +var ( + ErrEmailCampaignNotFound = errors.New("email campaign not found") +) + +type EmailCampaignRepository struct { + db *database.DB +} + +func (r *EmailCampaignRepository) NewTx(tx pgx.Tx) *EmailCampaignRepository { + txDB := &database.DB{ + Pool: r.db.Pool, + Query: sqlc.New(tx), + } + return &EmailCampaignRepository{db: txDB} +} + +func NewEmailCampaignRepository(db *database.DB) *EmailCampaignRepository { + return &EmailCampaignRepository{db: db} +} + +func (r *EmailCampaignRepository) CreateEmailCampaign( + ctx context.Context, + params sqlc.CreateEmailCampaignParams, +) (*sqlc.EmailCampaign, error) { + campaign, err := r.db.Query.CreateEmailCampaign(ctx, params) + if err != nil { + return nil, err + } + return &campaign, nil +} + +func (r *EmailCampaignRepository) GetEmailCampaignByID( + ctx context.Context, + params sqlc.GetEmailCampaignByIDParams, +) (*sqlc.EmailCampaign, error) { + campaign, err := r.db.Query.GetEmailCampaignByID(ctx, params) + if errors.Is(err, pgx.ErrNoRows) { + return nil, ErrEmailCampaignNotFound + } + if err != nil { + return nil, err + } + return &campaign, nil +} + +func (r *EmailCampaignRepository) ListEmailCampaigns( + ctx context.Context, + hackathonID string, +) ([]sqlc.EmailCampaign, error) { + return r.db.Query.ListEmailCampaigns(ctx, hackathonID) +} + +func (r *EmailCampaignRepository) UpdateEmailCampaign( + ctx context.Context, + params sqlc.UpdateEmailCampaignParams, +) (*sqlc.EmailCampaign, error) { + campaign, err := r.db.Query.UpdateEmailCampaign(ctx, params) + if errors.Is(err, pgx.ErrNoRows) { + return nil, ErrEmailCampaignNotFound + } + if err != nil { + return nil, err + } + return &campaign, nil +} + +func (r *EmailCampaignRepository) UpdateEmailCampaignStatus( + ctx context.Context, + params sqlc.UpdateEmailCampaignStatusParams, +) (*sqlc.EmailCampaign, error) { + campaign, err := r.db.Query.UpdateEmailCampaignStatus(ctx, params) + if errors.Is(err, pgx.ErrNoRows) { + return nil, ErrEmailCampaignNotFound + } + if err != nil { + return nil, err + } + return &campaign, nil +} diff --git a/apps/api/internal/database/sqlc/application_reviews.sql.go b/apps/api/internal/database/sqlc/application_reviews.sql.go new file mode 100644 index 00000000..25bd032d --- /dev/null +++ b/apps/api/internal/database/sqlc/application_reviews.sql.go @@ -0,0 +1,576 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: application_reviews.sql + +package sqlc + +import ( + "context" + "time" + + "github.com/google/uuid" +) + +const assignReviewerToApplications = `-- name: AssignReviewerToApplications :exec +INSERT INTO application_reviews ( + application_id, + reviewer_id +) +SELECT applications.id, $1::uuid FROM applications +WHERE applications.id = ANY($2::uuid[]) +ON CONFLICT DO NOTHING +` + +type AssignReviewerToApplicationsParams struct { + ReviewerID uuid.UUID `json:"reviewer_id"` + ApplicationIds []uuid.UUID `json:"application_ids"` +} + +func (q *Queries) AssignReviewerToApplications(ctx context.Context, arg AssignReviewerToApplicationsParams) error { + _, err := q.db.Exec(ctx, assignReviewerToApplications, arg.ReviewerID, arg.ApplicationIds) + return err +} + +const deleteAllApplicationReviews = `-- name: DeleteAllApplicationReviews :exec +DELETE FROM application_reviews +` + +func (q *Queries) DeleteAllApplicationReviews(ctx context.Context) error { + _, err := q.db.Exec(ctx, deleteAllApplicationReviews) + return err +} + +const deleteAllAutoDecisionRequests = `-- name: DeleteAllAutoDecisionRequests :exec +DELETE FROM application_auto_decision_requests +` + +func (q *Queries) DeleteAllAutoDecisionRequests(ctx context.Context) error { + _, err := q.db.Exec(ctx, deleteAllAutoDecisionRequests) + return err +} + +const deleteAutoDecisionRequest = `-- name: DeleteAutoDecisionRequest :exec +DELETE FROM application_auto_decision_requests +WHERE id = $1 AND reviewer_id = $2 +` + +type DeleteAutoDecisionRequestParams struct { + ID uuid.UUID `json:"id"` + ReviewerID uuid.UUID `json:"reviewer_id"` +} + +func (q *Queries) DeleteAutoDecisionRequest(ctx context.Context, arg DeleteAutoDecisionRequestParams) error { + _, err := q.db.Exec(ctx, deleteAutoDecisionRequest, arg.ID, arg.ReviewerID) + return err +} + +const getAutoDecisionRequestsCount = `-- name: GetAutoDecisionRequestsCount :one +SELECT COUNT(*) FROM application_auto_decision_requests +` + +func (q *Queries) GetAutoDecisionRequestsCount(ctx context.Context) (int64, error) { + row := q.db.QueryRow(ctx, getAutoDecisionRequestsCount) + var count int64 + err := row.Scan(&count) + return count, err +} + +const getReviewById = `-- name: GetReviewById :one +SELECT + ar.id, ar.application_id, ar.reviewer_id, ar.experience_rating, ar.passion_rating, ar.notes, ar.updated_by, ar.created_at, ar.updated_at, + aadr.requested_decision, + aadr.id AS decision_request_id, + aadr.justification AS decision_justification, + aadr.approved AS decision_approved, + aadr.decided_by AS decision_decided_by, + aadr.created_at AS decision_request_created_at, + applications.user_id, + applications.application +FROM application_reviews AS ar +JOIN applications ON applications.id = ar.application_id +LEFT JOIN application_auto_decision_requests AS aadr ON aadr.application_id = ar.application_id +WHERE ar.id = $1 +` + +type GetReviewByIdRow struct { + ID uuid.UUID `json:"id"` + ApplicationID uuid.UUID `json:"application_id"` + ReviewerID uuid.UUID `json:"reviewer_id"` + ExperienceRating *int32 `json:"experience_rating"` + PassionRating *int32 `json:"passion_rating"` + Notes *string `json:"notes"` + UpdatedBy *uuid.UUID `json:"updated_by"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + RequestedDecision NullApplicationAutoDecisionType `json:"requested_decision"` + DecisionRequestID *uuid.UUID `json:"decision_request_id"` + DecisionJustification *string `json:"decision_justification"` + DecisionApproved *bool `json:"decision_approved"` + DecisionDecidedBy *uuid.UUID `json:"decision_decided_by"` + DecisionRequestCreatedAt *time.Time `json:"decision_request_created_at"` + UserID uuid.UUID `json:"user_id"` + Application []byte `json:"application"` +} + +func (q *Queries) GetReviewById(ctx context.Context, reviewID uuid.UUID) (GetReviewByIdRow, error) { + row := q.db.QueryRow(ctx, getReviewById, reviewID) + var i GetReviewByIdRow + err := row.Scan( + &i.ID, + &i.ApplicationID, + &i.ReviewerID, + &i.ExperienceRating, + &i.PassionRating, + &i.Notes, + &i.UpdatedBy, + &i.CreatedAt, + &i.UpdatedAt, + &i.RequestedDecision, + &i.DecisionRequestID, + &i.DecisionJustification, + &i.DecisionApproved, + &i.DecisionDecidedBy, + &i.DecisionRequestCreatedAt, + &i.UserID, + &i.Application, + ) + return i, err +} + +const listApplicationReviewersById = `-- name: ListApplicationReviewersById :many +SELECT reviewer_id FROM application_reviews +WHERE application_id = $1 +` + +func (q *Queries) ListApplicationReviewersById(ctx context.Context, applicationID uuid.UUID) ([]uuid.UUID, error) { + rows, err := q.db.Query(ctx, listApplicationReviewersById, applicationID) + if err != nil { + return nil, err + } + defer rows.Close() + items := []uuid.UUID{} + for rows.Next() { + var reviewer_id uuid.UUID + if err := rows.Scan(&reviewer_id); err != nil { + return nil, err + } + items = append(items, reviewer_id) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listAutoDecisionRequests = `-- name: ListAutoDecisionRequests :many +SELECT + aadr.id, aadr.application_id, aadr.reviewer_id, aadr.requested_decision, aadr.justification, aadr.approved, aadr.decided_by, aadr.updated_at, aadr.created_at, + reviewer.name AS reviewer_name, + reviewer.image AS reviewer_image, + approver.id AS approver_id, + approver.name AS approver_name, + approver.image AS approver_image, + applicant.id AS user_id, + applicant.name AS user_name, + applicant.image AS user_image +FROM application_auto_decision_requests AS aadr +JOIN users AS reviewer + ON reviewer.id = aadr.reviewer_id +JOIN applications + ON applications.id = aadr.application_id +JOIN users AS applicant + ON applicant.id = applications.user_id +LEFT JOIN users AS approver + ON approver.id = aadr.decided_by +ORDER BY aadr.created_at DESC +` + +type ListAutoDecisionRequestsRow struct { + ID uuid.UUID `json:"id"` + ApplicationID uuid.UUID `json:"application_id"` + ReviewerID uuid.UUID `json:"reviewer_id"` + RequestedDecision ApplicationAutoDecisionType `json:"requested_decision"` + Justification *string `json:"justification"` + Approved *bool `json:"approved"` + DecidedBy *uuid.UUID `json:"decided_by"` + UpdatedAt time.Time `json:"updated_at"` + CreatedAt time.Time `json:"created_at"` + ReviewerName string `json:"reviewer_name"` + ReviewerImage *string `json:"reviewer_image"` + ApproverID *uuid.UUID `json:"approver_id"` + ApproverName *string `json:"approver_name"` + ApproverImage *string `json:"approver_image"` + UserID uuid.UUID `json:"user_id"` + UserName string `json:"user_name"` + UserImage *string `json:"user_image"` +} + +func (q *Queries) ListAutoDecisionRequests(ctx context.Context) ([]ListAutoDecisionRequestsRow, error) { + rows, err := q.db.Query(ctx, listAutoDecisionRequests) + if err != nil { + return nil, err + } + defer rows.Close() + items := []ListAutoDecisionRequestsRow{} + for rows.Next() { + var i ListAutoDecisionRequestsRow + if err := rows.Scan( + &i.ID, + &i.ApplicationID, + &i.ReviewerID, + &i.RequestedDecision, + &i.Justification, + &i.Approved, + &i.DecidedBy, + &i.UpdatedAt, + &i.CreatedAt, + &i.ReviewerName, + &i.ReviewerImage, + &i.ApproverID, + &i.ApproverName, + &i.ApproverImage, + &i.UserID, + &i.UserName, + &i.UserImage, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listReviewersAndProgress = `-- name: ListReviewersAndProgress :many +SELECT + reviewer.id, + reviewer.name, + reviewer.image, + COUNT(*) AS total_assigned, + COUNT(*) FILTER ( + WHERE ar.experience_rating IS NOT NULL AND ar.passion_rating IS NOT NULL + ) AS completed_count +FROM application_reviews AS ar +LEFT JOIN users AS reviewer + ON reviewer.id = ar.reviewer_id +GROUP BY + reviewer.id +` + +type ListReviewersAndProgressRow struct { + ID *uuid.UUID `json:"id"` + Name *string `json:"name"` + Image *string `json:"image"` + TotalAssigned int64 `json:"total_assigned"` + CompletedCount int64 `json:"completed_count"` +} + +func (q *Queries) ListReviewersAndProgress(ctx context.Context) ([]ListReviewersAndProgressRow, error) { + rows, err := q.db.Query(ctx, listReviewersAndProgress) + if err != nil { + return nil, err + } + defer rows.Close() + items := []ListReviewersAndProgressRow{} + for rows.Next() { + var i ListReviewersAndProgressRow + if err := rows.Scan( + &i.ID, + &i.Name, + &i.Image, + &i.TotalAssigned, + &i.CompletedCount, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listReviewsByReviewerId = `-- name: ListReviewsByReviewerId :many +SELECT + ar.id, ar.application_id, ar.reviewer_id, ar.experience_rating, ar.passion_rating, ar.notes, ar.updated_by, ar.created_at, ar.updated_at, + applications.user_id +FROM application_reviews ar +JOIN applications ON applications.id = ar.application_id +WHERE reviewer_id = $1 +ORDER BY application_id ASC +` + +type ListReviewsByReviewerIdRow struct { + ID uuid.UUID `json:"id"` + ApplicationID uuid.UUID `json:"application_id"` + ReviewerID uuid.UUID `json:"reviewer_id"` + ExperienceRating *int32 `json:"experience_rating"` + PassionRating *int32 `json:"passion_rating"` + Notes *string `json:"notes"` + UpdatedBy *uuid.UUID `json:"updated_by"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + UserID uuid.UUID `json:"user_id"` +} + +func (q *Queries) ListReviewsByReviewerId(ctx context.Context, reviewerID uuid.UUID) ([]ListReviewsByReviewerIdRow, error) { + rows, err := q.db.Query(ctx, listReviewsByReviewerId, reviewerID) + if err != nil { + return nil, err + } + defer rows.Close() + items := []ListReviewsByReviewerIdRow{} + for rows.Next() { + var i ListReviewsByReviewerIdRow + if err := rows.Scan( + &i.ID, + &i.ApplicationID, + &i.ReviewerID, + &i.ExperienceRating, + &i.PassionRating, + &i.Notes, + &i.UpdatedBy, + &i.CreatedAt, + &i.UpdatedAt, + &i.UserID, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const requestAutoDecision = `-- name: RequestAutoDecision :one +INSERT INTO application_auto_decision_requests (application_id, reviewer_id, requested_decision, justification, approved, decided_by) +VALUES ($1, $2, $3, $4, $5, $6) RETURNING id, application_id, reviewer_id, requested_decision, justification, approved, decided_by, updated_at, created_at +` + +type RequestAutoDecisionParams struct { + ApplicationID uuid.UUID `json:"application_id"` + ReviewerID uuid.UUID `json:"reviewer_id"` + RequestedDecision ApplicationAutoDecisionType `json:"requested_decision"` + Justification *string `json:"justification"` + Approved *bool `json:"approved"` + DecidedBy *uuid.UUID `json:"decided_by"` +} + +func (q *Queries) RequestAutoDecision(ctx context.Context, arg RequestAutoDecisionParams) (ApplicationAutoDecisionRequest, error) { + row := q.db.QueryRow(ctx, requestAutoDecision, + arg.ApplicationID, + arg.ReviewerID, + arg.RequestedDecision, + arg.Justification, + arg.Approved, + arg.DecidedBy, + ) + var i ApplicationAutoDecisionRequest + err := row.Scan( + &i.ID, + &i.ApplicationID, + &i.ReviewerID, + &i.RequestedDecision, + &i.Justification, + &i.Approved, + &i.DecidedBy, + &i.UpdatedAt, + &i.CreatedAt, + ) + return i, err +} + +const searchAutoDecisionRequests = `-- name: SearchAutoDecisionRequests :many +SELECT + aadr.id, aadr.application_id, aadr.reviewer_id, aadr.requested_decision, aadr.justification, aadr.approved, aadr.decided_by, aadr.updated_at, aadr.created_at, + reviewer.name AS reviewer_name, + reviewer.image AS reviewer_image, + approver.id AS approver_id, + approver.name AS approver_name, + approver.image AS approver_image, + applicant.id AS user_id, + applicant.name AS user_name, + applicant.image AS user_image +FROM application_auto_decision_requests AS aadr +JOIN users AS reviewer + ON reviewer.id = aadr.reviewer_id +JOIN applications + ON applications.id = aadr.application_id +JOIN users AS applicant + ON applicant.id = applications.user_id +LEFT JOIN users AS approver + ON approver.id = aadr.decided_by +WHERE (LOWER(reviewer.name) LIKE LOWER('%' || COALESCE($1, '') || '%') + OR LOWER(applicant.name) LIKE LOWER('%' || COALESCE($1, '') || '%')) + AND ( + $2::text = 'all' + OR ($2::text = 'pending' AND aadr.approved IS NULL) + OR ($2::text = 'approved' AND aadr.approved = true) + OR ($2::text = 'denied' AND aadr.approved = false) + ) + AND ( + $3::application_auto_decision_type IS NULL + OR aadr.requested_decision = $3::application_auto_decision_type + ) +ORDER BY aadr.created_at DESC +LIMIT $5 OFFSET $4 +` + +type SearchAutoDecisionRequestsParams struct { + Search *string `json:"search"` + Approved string `json:"approved"` + Decision NullApplicationAutoDecisionType `json:"decision"` + Offset int32 `json:"offset"` + Limit int32 `json:"limit"` +} + +type SearchAutoDecisionRequestsRow struct { + ID uuid.UUID `json:"id"` + ApplicationID uuid.UUID `json:"application_id"` + ReviewerID uuid.UUID `json:"reviewer_id"` + RequestedDecision ApplicationAutoDecisionType `json:"requested_decision"` + Justification *string `json:"justification"` + Approved *bool `json:"approved"` + DecidedBy *uuid.UUID `json:"decided_by"` + UpdatedAt time.Time `json:"updated_at"` + CreatedAt time.Time `json:"created_at"` + ReviewerName string `json:"reviewer_name"` + ReviewerImage *string `json:"reviewer_image"` + ApproverID *uuid.UUID `json:"approver_id"` + ApproverName *string `json:"approver_name"` + ApproverImage *string `json:"approver_image"` + UserID uuid.UUID `json:"user_id"` + UserName string `json:"user_name"` + UserImage *string `json:"user_image"` +} + +func (q *Queries) SearchAutoDecisionRequests(ctx context.Context, arg SearchAutoDecisionRequestsParams) ([]SearchAutoDecisionRequestsRow, error) { + rows, err := q.db.Query(ctx, searchAutoDecisionRequests, + arg.Search, + arg.Approved, + arg.Decision, + arg.Offset, + arg.Limit, + ) + if err != nil { + return nil, err + } + defer rows.Close() + items := []SearchAutoDecisionRequestsRow{} + for rows.Next() { + var i SearchAutoDecisionRequestsRow + if err := rows.Scan( + &i.ID, + &i.ApplicationID, + &i.ReviewerID, + &i.RequestedDecision, + &i.Justification, + &i.Approved, + &i.DecidedBy, + &i.UpdatedAt, + &i.CreatedAt, + &i.ReviewerName, + &i.ReviewerImage, + &i.ApproverID, + &i.ApproverName, + &i.ApproverImage, + &i.UserID, + &i.UserName, + &i.UserImage, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const updateApplicationReview = `-- name: UpdateApplicationReview :exec +UPDATE application_reviews +SET + experience_rating = CASE WHEN $1::boolean THEN $2 ELSE experience_rating END, + passion_rating = CASE WHEN $3::boolean THEN $4 ELSE passion_rating END, + notes = CASE WHEN $5::boolean THEN $6 ELSE notes END, + updated_by = CASE WHEN $7::boolean THEN $8 ELSE updated_by END, + updated_at = NOW() +WHERE + id = $9 AND + reviewer_id = $10 +` + +type UpdateApplicationReviewParams struct { + ExperienceRatingDoUpdate bool `json:"experience_rating_do_update"` + ExperienceRating *int32 `json:"experience_rating"` + PassionRatingDoUpdate bool `json:"passion_rating_do_update"` + PassionRating *int32 `json:"passion_rating"` + NotesDoUpdate bool `json:"notes_do_update"` + Notes *string `json:"notes"` + UpdatedByDoUpdate bool `json:"updated_by_do_update"` + UpdatedBy *uuid.UUID `json:"updated_by"` + ID uuid.UUID `json:"id"` + ReviewerID uuid.UUID `json:"reviewer_id"` +} + +func (q *Queries) UpdateApplicationReview(ctx context.Context, arg UpdateApplicationReviewParams) error { + _, err := q.db.Exec(ctx, updateApplicationReview, + arg.ExperienceRatingDoUpdate, + arg.ExperienceRating, + arg.PassionRatingDoUpdate, + arg.PassionRating, + arg.NotesDoUpdate, + arg.Notes, + arg.UpdatedByDoUpdate, + arg.UpdatedBy, + arg.ID, + arg.ReviewerID, + ) + return err +} + +const updateAutoDecisionRequest = `-- name: UpdateAutoDecisionRequest :exec +UPDATE application_auto_decision_requests +SET + requested_decision = CASE WHEN $1::boolean AND $2 <> '' THEN $2::application_auto_decision_type ELSE requested_decision END, + justification = CASE WHEN $3::boolean THEN $4 ELSE justification END, + approved = CASE WHEN $5::boolean THEN $6 ELSE approved END, + decided_by = CASE WHEN $7::boolean THEN $8 ELSE decided_by END +WHERE id = $9 AND reviewer_id = $10 +` + +type UpdateAutoDecisionRequestParams struct { + RequestedDecisionDoUpdate bool `json:"requested_decision_do_update"` + RequestedDecision interface{} `json:"requested_decision"` + JustificationDoUpdate bool `json:"justification_do_update"` + Justification *string `json:"justification"` + ApprovedDoUpdate bool `json:"approved_do_update"` + Approved *bool `json:"approved"` + ApprovedByDoUpdate bool `json:"approved_by_do_update"` + DecidedBy *uuid.UUID `json:"decided_by"` + ID uuid.UUID `json:"id"` + ReviewerID uuid.UUID `json:"reviewer_id"` +} + +func (q *Queries) UpdateAutoDecisionRequest(ctx context.Context, arg UpdateAutoDecisionRequestParams) error { + _, err := q.db.Exec(ctx, updateAutoDecisionRequest, + arg.RequestedDecisionDoUpdate, + arg.RequestedDecision, + arg.JustificationDoUpdate, + arg.Justification, + arg.ApprovedDoUpdate, + arg.Approved, + arg.ApprovedByDoUpdate, + arg.DecidedBy, + arg.ID, + arg.ReviewerID, + ) + return err +} diff --git a/apps/api/internal/database/sqlc/stats.sql.go b/apps/api/internal/database/sqlc/application_stats.sql.go similarity index 72% rename from apps/api/internal/database/sqlc/stats.sql.go rename to apps/api/internal/database/sqlc/application_stats.sql.go index 2703cbfb..fea3aa2b 100644 --- a/apps/api/internal/database/sqlc/stats.sql.go +++ b/apps/api/internal/database/sqlc/application_stats.sql.go @@ -1,7 +1,7 @@ // Code generated by sqlc. DO NOT EDIT. // versions: // sqlc v1.30.0 -// source: stats.sql +// source: application_stats.sql package sqlc @@ -11,7 +11,79 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) -const getApplicationAgeSplit = `-- name: GetApplicationAgeSplit :one +const getApplicationStatuses = `-- name: GetApplicationStatuses :one +SELECT + COUNT(*) FILTER (WHERE status = 'started') AS started, + COUNT(*) FILTER (WHERE status = 'submitted') AS submitted, + COUNT(*) FILTER (WHERE status = 'under_review') AS under_review, + COUNT(*) FILTER (WHERE status = 'accepted') AS accepted, + COUNT(*) FILTER (WHERE status = 'rejected') AS rejected, + COUNT(*) FILTER (WHERE status = 'waitlisted') AS waitlisted, + COUNT(*) FILTER (WHERE status = 'withdrawn') AS withdrawn +FROM applications +` + +type GetApplicationStatusesRow struct { + Started int64 `json:"started"` + Submitted int64 `json:"submitted"` + UnderReview int64 `json:"under_review"` + Accepted int64 `json:"accepted"` + Rejected int64 `json:"rejected"` + Waitlisted int64 `json:"waitlisted"` + Withdrawn int64 `json:"withdrawn"` +} + +func (q *Queries) GetApplicationStatuses(ctx context.Context) (GetApplicationStatusesRow, error) { + row := q.db.QueryRow(ctx, getApplicationStatuses) + var i GetApplicationStatusesRow + err := row.Scan( + &i.Started, + &i.Submitted, + &i.UnderReview, + &i.Accepted, + &i.Rejected, + &i.Waitlisted, + &i.Withdrawn, + ) + return i, err +} + +const getSubmissionTimes = `-- name: GetSubmissionTimes :many +SELECT + date_trunc('day', submitted_at AT TIME ZONE 'US/Eastern')::date AS day, + COUNT(*) AS count +FROM applications +WHERE submitted_at IS NOT NULL +GROUP BY day +ORDER By day +` + +type GetSubmissionTimesRow struct { + Day pgtype.Date `json:"day"` + Count int64 `json:"count"` +} + +func (q *Queries) GetSubmissionTimes(ctx context.Context) ([]GetSubmissionTimesRow, error) { + rows, err := q.db.Query(ctx, getSubmissionTimes) + if err != nil { + return nil, err + } + defer rows.Close() + items := []GetSubmissionTimesRow{} + for rows.Next() { + var i GetSubmissionTimesRow + if err := rows.Scan(&i.Day, &i.Count); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getSubmittedApplicationAges = `-- name: GetSubmittedApplicationAges :one SELECT COUNT(*) FILTER (WHERE (application->>'age')::int < 18) AS underage, COUNT(*) FILTER (WHERE (application->>'age')::int = 18) AS age_18, @@ -24,7 +96,7 @@ FROM applications WHERE status <> 'started' AND status IS NOT NULL ` -type GetApplicationAgeSplitRow struct { +type GetSubmittedApplicationAgesRow struct { Underage int64 `json:"underage"` Age18 int64 `json:"age_18"` Age19 int64 `json:"age_19"` @@ -34,9 +106,9 @@ type GetApplicationAgeSplitRow struct { Age23Plus int64 `json:"age_23_plus"` } -func (q *Queries) GetApplicationAgeSplit(ctx context.Context) (GetApplicationAgeSplitRow, error) { - row := q.db.QueryRow(ctx, getApplicationAgeSplit) - var i GetApplicationAgeSplitRow +func (q *Queries) GetSubmittedApplicationAges(ctx context.Context) (GetSubmittedApplicationAgesRow, error) { + row := q.db.QueryRow(ctx, getSubmittedApplicationAges) + var i GetSubmittedApplicationAgesRow err := row.Scan( &i.Underage, &i.Age18, @@ -49,7 +121,7 @@ func (q *Queries) GetApplicationAgeSplit(ctx context.Context) (GetApplicationAge return i, err } -const getApplicationGenderSplit = `-- name: GetApplicationGenderSplit :one +const getSubmittedApplicationGenders = `-- name: GetSubmittedApplicationGenders :one SELECT COUNT(*) FILTER (WHERE application->>'gender' = 'man') AS male, @@ -60,7 +132,7 @@ FROM applications WHERE status <> 'started' AND status IS NOT NULL ` -type GetApplicationGenderSplitRow struct { +type GetSubmittedApplicationGendersRow struct { Male int64 `json:"male"` Female int64 `json:"female"` NonBinary int64 `json:"non_binary"` @@ -68,9 +140,9 @@ type GetApplicationGenderSplitRow struct { } // Queries used for statistics, mainly used by overview dashboards etc -func (q *Queries) GetApplicationGenderSplit(ctx context.Context) (GetApplicationGenderSplitRow, error) { - row := q.db.QueryRow(ctx, getApplicationGenderSplit) - var i GetApplicationGenderSplitRow +func (q *Queries) GetSubmittedApplicationGenders(ctx context.Context) (GetSubmittedApplicationGendersRow, error) { + row := q.db.QueryRow(ctx, getSubmittedApplicationGenders) + var i GetSubmittedApplicationGendersRow err := row.Scan( &i.Male, &i.Female, @@ -80,7 +152,7 @@ func (q *Queries) GetApplicationGenderSplit(ctx context.Context) (GetApplication return i, err } -const getApplicationMajorSplit = `-- name: GetApplicationMajorSplit :many +const getSubmittedApplicationMajors = `-- name: GetSubmittedApplicationMajors :many SELECT trim(major) AS major, COUNT(*) AS count @@ -91,20 +163,20 @@ GROUP BY trim(major) ORDER BY count DESC ` -type GetApplicationMajorSplitRow struct { +type GetSubmittedApplicationMajorsRow struct { Major string `json:"major"` Count int64 `json:"count"` } -func (q *Queries) GetApplicationMajorSplit(ctx context.Context) ([]GetApplicationMajorSplitRow, error) { - rows, err := q.db.Query(ctx, getApplicationMajorSplit) +func (q *Queries) GetSubmittedApplicationMajors(ctx context.Context) ([]GetSubmittedApplicationMajorsRow, error) { + rows, err := q.db.Query(ctx, getSubmittedApplicationMajors) if err != nil { return nil, err } defer rows.Close() - items := []GetApplicationMajorSplitRow{} + items := []GetSubmittedApplicationMajorsRow{} for rows.Next() { - var i GetApplicationMajorSplitRow + var i GetSubmittedApplicationMajorsRow if err := rows.Scan(&i.Major, &i.Count); err != nil { return nil, err } @@ -116,7 +188,7 @@ func (q *Queries) GetApplicationMajorSplit(ctx context.Context) ([]GetApplicatio return items, nil } -const getApplicationRaceSplit = `-- name: GetApplicationRaceSplit :many +const getSubmittedApplicationRaces = `-- name: GetSubmittedApplicationRaces :many SELECT CASE WHEN application->>'race' IS NOT NULL AND application->>'race' <> '' THEN application->>'race' @@ -135,20 +207,20 @@ GROUP BY ORDER BY count DESC ` -type GetApplicationRaceSplitRow struct { +type GetSubmittedApplicationRacesRow struct { RaceGroup string `json:"race_group"` Count int64 `json:"count"` } -func (q *Queries) GetApplicationRaceSplit(ctx context.Context) ([]GetApplicationRaceSplitRow, error) { - rows, err := q.db.Query(ctx, getApplicationRaceSplit) +func (q *Queries) GetSubmittedApplicationRaces(ctx context.Context) ([]GetSubmittedApplicationRacesRow, error) { + rows, err := q.db.Query(ctx, getSubmittedApplicationRaces) if err != nil { return nil, err } defer rows.Close() - items := []GetApplicationRaceSplitRow{} + items := []GetSubmittedApplicationRacesRow{} for rows.Next() { - var i GetApplicationRaceSplitRow + var i GetSubmittedApplicationRacesRow if err := rows.Scan(&i.RaceGroup, &i.Count); err != nil { return nil, err } @@ -160,7 +232,7 @@ func (q *Queries) GetApplicationRaceSplit(ctx context.Context) ([]GetApplication return items, nil } -const getApplicationSchoolSplit = `-- name: GetApplicationSchoolSplit :many +const getSubmittedApplicationSchools = `-- name: GetSubmittedApplicationSchools :many SELECT (application->>'school')::text AS school, COUNT(*) AS count @@ -170,20 +242,20 @@ GROUP BY (application->>'school')::text ORDER BY count DESC ` -type GetApplicationSchoolSplitRow struct { +type GetSubmittedApplicationSchoolsRow struct { School string `json:"school"` Count int64 `json:"count"` } -func (q *Queries) GetApplicationSchoolSplit(ctx context.Context) ([]GetApplicationSchoolSplitRow, error) { - rows, err := q.db.Query(ctx, getApplicationSchoolSplit) +func (q *Queries) GetSubmittedApplicationSchools(ctx context.Context) ([]GetSubmittedApplicationSchoolsRow, error) { + rows, err := q.db.Query(ctx, getSubmittedApplicationSchools) if err != nil { return nil, err } defer rows.Close() - items := []GetApplicationSchoolSplitRow{} + items := []GetSubmittedApplicationSchoolsRow{} for rows.Next() { - var i GetApplicationSchoolSplitRow + var i GetSubmittedApplicationSchoolsRow if err := rows.Scan(&i.School, &i.Count); err != nil { return nil, err } @@ -194,75 +266,3 @@ func (q *Queries) GetApplicationSchoolSplit(ctx context.Context) ([]GetApplicati } return items, nil } - -const getApplicationStatusSplit = `-- name: GetApplicationStatusSplit :one -SELECT - COUNT(*) FILTER (WHERE status = 'started') AS started, - COUNT(*) FILTER (WHERE status = 'submitted') AS submitted, - COUNT(*) FILTER (WHERE status = 'under_review') AS under_review, - COUNT(*) FILTER (WHERE status = 'accepted') AS accepted, - COUNT(*) FILTER (WHERE status = 'rejected') AS rejected, - COUNT(*) FILTER (WHERE status = 'waitlisted') AS waitlisted, - COUNT(*) FILTER (WHERE status = 'withdrawn') AS withdrawn -FROM applications -` - -type GetApplicationStatusSplitRow struct { - Started int64 `json:"started"` - Submitted int64 `json:"submitted"` - UnderReview int64 `json:"under_review"` - Accepted int64 `json:"accepted"` - Rejected int64 `json:"rejected"` - Waitlisted int64 `json:"waitlisted"` - Withdrawn int64 `json:"withdrawn"` -} - -func (q *Queries) GetApplicationStatusSplit(ctx context.Context) (GetApplicationStatusSplitRow, error) { - row := q.db.QueryRow(ctx, getApplicationStatusSplit) - var i GetApplicationStatusSplitRow - err := row.Scan( - &i.Started, - &i.Submitted, - &i.UnderReview, - &i.Accepted, - &i.Rejected, - &i.Waitlisted, - &i.Withdrawn, - ) - return i, err -} - -const getSubmissionTimes = `-- name: GetSubmissionTimes :many -SELECT - date_trunc('day', submitted_at AT TIME ZONE 'US/Eastern')::date AS day, - COUNT(*) AS count -FROM applications -WHERE submitted_at IS NOT NULL -GROUP BY day -ORDER By day -` - -type GetSubmissionTimesRow struct { - Day pgtype.Date `json:"day"` - Count int64 `json:"count"` -} - -func (q *Queries) GetSubmissionTimes(ctx context.Context) ([]GetSubmissionTimesRow, error) { - rows, err := q.db.Query(ctx, getSubmissionTimes) - if err != nil { - return nil, err - } - defer rows.Close() - items := []GetSubmissionTimesRow{} - for rows.Next() { - var i GetSubmissionTimesRow - if err := rows.Scan(&i.Day, &i.Count); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} diff --git a/apps/api/internal/database/sqlc/applications.sql.go b/apps/api/internal/database/sqlc/applications.sql.go index 795268a1..5553ba00 100644 --- a/apps/api/internal/database/sqlc/applications.sql.go +++ b/apps/api/internal/database/sqlc/applications.sql.go @@ -12,25 +12,41 @@ import ( "github.com/google/uuid" ) -const assignApplicationsToReviewer = `-- name: AssignApplicationsToReviewer :exec +const acceptWaitlistedApplications = `-- name: AcceptWaitlistedApplications :many UPDATE applications -SET assigned_reviewer_id = $1::uuid, - status = 'under_review' -WHERE user_id = ANY($2::uuid[]) +SET waitlist_join_time = NULL, + status = 'accepted' +WHERE user_id IN ( + SELECT user_id FROM applications + WHERE status = 'waitlisted' + ORDER BY waitlist_join_time ASC + LIMIT $1::int +) +RETURNING user_id ` -type AssignApplicationsToReviewerParams struct { - ReviewerID uuid.UUID `json:"reviewer_id"` - ApplicationIds []uuid.UUID `json:"application_ids"` -} - -func (q *Queries) AssignApplicationsToReviewer(ctx context.Context, arg AssignApplicationsToReviewerParams) error { - _, err := q.db.Exec(ctx, assignApplicationsToReviewer, arg.ReviewerID, arg.ApplicationIds) - return err +func (q *Queries) AcceptWaitlistedApplications(ctx context.Context, acceptancecount int32) ([]uuid.UUID, error) { + rows, err := q.db.Query(ctx, acceptWaitlistedApplications, acceptancecount) + if err != nil { + return nil, err + } + defer rows.Close() + items := []uuid.UUID{} + for rows.Next() { + var user_id uuid.UUID + if err := rows.Scan(&user_id); err != nil { + return nil, err + } + items = append(items, user_id) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil } const createApplication = `-- name: CreateApplication :one -INSERT INTO applications (user_id, hackathon_id, is_early) VALUES ($1, $2, $3) RETURNING user_id, status, application, created_at, saved_at, updated_at, submitted_at, experience_rating, passion_rating, assigned_reviewer_id, waitlist_join_time, hackathon_id, is_early +INSERT INTO applications (user_id, hackathon_id, is_early) VALUES ($1, $2, $3) RETURNING user_id, status, application, created_at, saved_at, updated_at, submitted_at, hackathon_id, is_early, id ` type CreateApplicationParams struct { @@ -50,27 +66,46 @@ func (q *Queries) CreateApplication(ctx context.Context, arg CreateApplicationPa &i.SavedAt, &i.UpdatedAt, &i.SubmittedAt, - &i.ExperienceRating, - &i.PassionRating, - &i.AssignedReviewerID, - &i.WaitlistJoinTime, &i.HackathonID, &i.IsEarly, + &i.ID, ) return i, err } -const deleteApplication = `-- name: DeleteApplication :exec -DELETE FROM applications WHERE user_id = $1 +const deleteApplicationById = `-- name: DeleteApplicationById :exec +DELETE FROM applications WHERE id = $1 ` -func (q *Queries) DeleteApplication(ctx context.Context, userID uuid.UUID) error { - _, err := q.db.Exec(ctx, deleteApplication, userID) +func (q *Queries) DeleteApplicationById(ctx context.Context, id uuid.UUID) error { + _, err := q.db.Exec(ctx, deleteApplicationById, id) return err } +const getApplicationById = `-- name: GetApplicationById :one +SELECT user_id, status, application, created_at, saved_at, updated_at, submitted_at, hackathon_id, is_early, id FROM applications WHERE id = $1 +` + +func (q *Queries) GetApplicationById(ctx context.Context, id uuid.UUID) (Application, error) { + row := q.db.QueryRow(ctx, getApplicationById, id) + var i Application + err := row.Scan( + &i.UserID, + &i.Status, + &i.Application, + &i.CreatedAt, + &i.SavedAt, + &i.UpdatedAt, + &i.SubmittedAt, + &i.HackathonID, + &i.IsEarly, + &i.ID, + ) + return i, err +} + const getApplicationByUserId = `-- name: GetApplicationByUserId :one -SELECT user_id, status, application, created_at, saved_at, updated_at, submitted_at, experience_rating, passion_rating, assigned_reviewer_id, waitlist_join_time, hackathon_id, is_early FROM applications WHERE user_id = $1 +SELECT user_id, status, application, created_at, saved_at, updated_at, submitted_at, hackathon_id, is_early, id FROM applications WHERE user_id = $1 ` func (q *Queries) GetApplicationByUserId(ctx context.Context, userID uuid.UUID) (Application, error) { @@ -84,32 +119,123 @@ func (q *Queries) GetApplicationByUserId(ctx context.Context, userID uuid.UUID) &i.SavedAt, &i.UpdatedAt, &i.SubmittedAt, - &i.ExperienceRating, - &i.PassionRating, - &i.AssignedReviewerID, - &i.WaitlistJoinTime, &i.HackathonID, &i.IsEarly, + &i.ID, ) return i, err } -const joinWaitlist = `-- name: JoinWaitlist :exec -UPDATE applications -SET waitlist_join_time = COALESCE(waitlist_join_time, NOW()), - status = 'waitlisted' -WHERE user_id = $1 +const getApplicationsCount = `-- name: GetApplicationsCount :one +SELECT COUNT(*) FROM applications WHERE hackathon_id = $1 ` -func (q *Queries) JoinWaitlist(ctx context.Context, userID uuid.UUID) error { - _, err := q.db.Exec(ctx, joinWaitlist, userID) - return err +func (q *Queries) GetApplicationsCount(ctx context.Context, hackathonID string) (int64, error) { + row := q.db.QueryRow(ctx, getApplicationsCount, hackathonID) + var count int64 + err := row.Scan(&count) + return count, err } -const listAdmissionCandidates = `-- name: ListAdmissionCandidates :many -SELECT a.user_id, - a.passion_rating, - a.experience_rating, +const getExtendedApplicationById = `-- name: GetExtendedApplicationById :one +SELECT + a.user_id, a.status, a.application, a.created_at, a.saved_at, a.updated_at, a.submitted_at, a.hackathon_id, a.is_early, a.id, + ar.id AS review_id, + ar.experience_rating, + ar.passion_rating, + ar.notes, + ar.updated_at AS review_updated_at, + ar.updated_by AS review_updated_by, + reviewer.id AS reviewer_id, + reviewer.name AS reviewer_name, + reviewer.image AS reviewer_image, + applicant.name AS user_name, + applicant.image AS user_image, + applicant.email AS user_email, + aadr.requested_decision, + aadr.id AS auto_decision_request_id, + aadr.justification AS decision_justification, + aadr.approved AS decision_approved, + aadr.decided_by, + aadr.created_at AS decision_request_created_at +FROM applications a +JOIN users AS applicant ON applicant.id = a.user_id +LEFT JOIN application_reviews AS ar ON ar.application_id = a.id +LEFT JOIN users AS reviewer ON reviewer.id = ar.reviewer_id +LEFT JOIN application_auto_decision_requests as aadr ON aadr.application_id = a.id +WHERE a.id = $1 +` + +type GetExtendedApplicationByIdRow struct { + UserID uuid.UUID `json:"user_id"` + Status ApplicationStatus `json:"status"` + Application []byte `json:"application"` + CreatedAt time.Time `json:"created_at"` + SavedAt time.Time `json:"saved_at"` + UpdatedAt time.Time `json:"updated_at"` + SubmittedAt *time.Time `json:"submitted_at"` + HackathonID string `json:"hackathon_id"` + IsEarly bool `json:"is_early"` + ID uuid.UUID `json:"id"` + ReviewID *uuid.UUID `json:"review_id"` + ExperienceRating *int32 `json:"experience_rating"` + PassionRating *int32 `json:"passion_rating"` + Notes *string `json:"notes"` + ReviewUpdatedAt *time.Time `json:"review_updated_at"` + ReviewUpdatedBy *uuid.UUID `json:"review_updated_by"` + ReviewerID *uuid.UUID `json:"reviewer_id"` + ReviewerName *string `json:"reviewer_name"` + ReviewerImage *string `json:"reviewer_image"` + UserName string `json:"user_name"` + UserImage *string `json:"user_image"` + UserEmail *string `json:"user_email"` + RequestedDecision NullApplicationAutoDecisionType `json:"requested_decision"` + AutoDecisionRequestID *uuid.UUID `json:"auto_decision_request_id"` + DecisionJustification *string `json:"decision_justification"` + DecisionApproved *bool `json:"decision_approved"` + DecidedBy *uuid.UUID `json:"decided_by"` + DecisionRequestCreatedAt *time.Time `json:"decision_request_created_at"` +} + +func (q *Queries) GetExtendedApplicationById(ctx context.Context, id uuid.UUID) (GetExtendedApplicationByIdRow, error) { + row := q.db.QueryRow(ctx, getExtendedApplicationById, id) + var i GetExtendedApplicationByIdRow + err := row.Scan( + &i.UserID, + &i.Status, + &i.Application, + &i.CreatedAt, + &i.SavedAt, + &i.UpdatedAt, + &i.SubmittedAt, + &i.HackathonID, + &i.IsEarly, + &i.ID, + &i.ReviewID, + &i.ExperienceRating, + &i.PassionRating, + &i.Notes, + &i.ReviewUpdatedAt, + &i.ReviewUpdatedBy, + &i.ReviewerID, + &i.ReviewerName, + &i.ReviewerImage, + &i.UserName, + &i.UserImage, + &i.UserEmail, + &i.RequestedDecision, + &i.AutoDecisionRequestID, + &i.DecisionJustification, + &i.DecisionApproved, + &i.DecidedBy, + &i.DecisionRequestCreatedAt, + ) + return i, err +} + +const listApplicationsUnderReviewWithTeamIds = `-- name: ListApplicationsUnderReviewWithTeamIds :many +SELECT + a.user_id, a.application, t.id as team_id FROM applications a @@ -118,34 +244,24 @@ LEFT JOIN team_members tm LEFT JOIN teams t ON t.id = tm.team_id WHERE a.status = 'under_review' - AND a.passion_rating IS NOT NULL - AND a.experience_rating IS NOT NULL ` -type ListAdmissionCandidatesRow struct { - UserID uuid.UUID `json:"user_id"` - PassionRating *int32 `json:"passion_rating"` - ExperienceRating *int32 `json:"experience_rating"` - Application []byte `json:"application"` - TeamID *uuid.UUID `json:"team_id"` +type ListApplicationsUnderReviewWithTeamIdsRow struct { + UserID uuid.UUID `json:"user_id"` + Application []byte `json:"application"` + TeamID *uuid.UUID `json:"team_id"` } -func (q *Queries) ListAdmissionCandidates(ctx context.Context) ([]ListAdmissionCandidatesRow, error) { - rows, err := q.db.Query(ctx, listAdmissionCandidates) +func (q *Queries) ListApplicationsUnderReviewWithTeamIds(ctx context.Context) ([]ListApplicationsUnderReviewWithTeamIdsRow, error) { + rows, err := q.db.Query(ctx, listApplicationsUnderReviewWithTeamIds) if err != nil { return nil, err } defer rows.Close() - items := []ListAdmissionCandidatesRow{} + items := []ListApplicationsUnderReviewWithTeamIdsRow{} for rows.Next() { - var i ListAdmissionCandidatesRow - if err := rows.Scan( - &i.UserID, - &i.PassionRating, - &i.ExperienceRating, - &i.Application, - &i.TeamID, - ); err != nil { + var i ListApplicationsUnderReviewWithTeamIdsRow + if err := rows.Scan(&i.UserID, &i.Application, &i.TeamID); err != nil { return nil, err } items = append(items, i) @@ -156,32 +272,25 @@ func (q *Queries) ListAdmissionCandidates(ctx context.Context) ([]ListAdmissionC return items, nil } -const listApplicationByReviewer = `-- name: ListApplicationByReviewer :many -SELECT user_id, passion_rating, experience_rating FROM applications -WHERE assigned_reviewer_id = $1 - AND status IN ('under_review') -ORDER BY user_id ASC +const listUnderReviewApplicationIds = `-- name: ListUnderReviewApplicationIds :many +SELECT id FROM applications +WHERE status = 'under_review' AND hackathon_id = $1 +ORDER BY id ASC ` -type ListApplicationByReviewerRow struct { - UserID uuid.UUID `json:"user_id"` - PassionRating *int32 `json:"passion_rating"` - ExperienceRating *int32 `json:"experience_rating"` -} - -func (q *Queries) ListApplicationByReviewer(ctx context.Context, assignedReviewerID *uuid.UUID) ([]ListApplicationByReviewerRow, error) { - rows, err := q.db.Query(ctx, listApplicationByReviewer, assignedReviewerID) +func (q *Queries) ListUnderReviewApplicationIds(ctx context.Context, hackathonID string) ([]uuid.UUID, error) { + rows, err := q.db.Query(ctx, listUnderReviewApplicationIds, hackathonID) if err != nil { return nil, err } defer rows.Close() - items := []ListApplicationByReviewerRow{} + items := []uuid.UUID{} for rows.Next() { - var i ListApplicationByReviewerRow - if err := rows.Scan(&i.UserID, &i.PassionRating, &i.ExperienceRating); err != nil { + var id uuid.UUID + if err := rows.Scan(&id); err != nil { return nil, err } - items = append(items, i) + items = append(items, id) } if err := rows.Err(); err != nil { return nil, err @@ -189,121 +298,98 @@ func (q *Queries) ListApplicationByReviewer(ctx context.Context, assignedReviewe return items, nil } -const listAvailableApplications = `-- name: ListAvailableApplications :many - -SELECT user_id FROM applications +const markSubmittedApplicationsAsUnderReview = `-- name: MarkSubmittedApplicationsAsUnderReview :exec +UPDATE applications +SET status = 'under_review' WHERE status = 'submitted' - AND experience_rating IS NULL - AND passion_rating IS NULL -ORDER BY - user_id ASC ` -// An application is considered "available" if the application has a status of submitted and has not been reviewed yet. -// For optimization purposes, we only select the application IDs. -func (q *Queries) ListAvailableApplications(ctx context.Context) ([]uuid.UUID, error) { - rows, err := q.db.Query(ctx, listAvailableApplications) - if err != nil { - return nil, err - } - defer rows.Close() - items := []uuid.UUID{} - for rows.Next() { - var user_id uuid.UUID - if err := rows.Scan(&user_id); err != nil { - return nil, err - } - items = append(items, user_id) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil +func (q *Queries) MarkSubmittedApplicationsAsUnderReview(ctx context.Context) error { + _, err := q.db.Exec(ctx, markSubmittedApplicationsAsUnderReview) + return err } -const listNonReviewedApplications = `-- name: ListNonReviewedApplications :many -SELECT user_id -FROM applications +const resetApplicationsToSubmitted = `-- name: ResetApplicationsToSubmitted :exec +UPDATE applications +SET status = 'submitted' WHERE status = 'under_review' - AND (passion_rating IS NULL OR experience_rating IS NULL) ` -func (q *Queries) ListNonReviewedApplications(ctx context.Context) ([]uuid.UUID, error) { - rows, err := q.db.Query(ctx, listNonReviewedApplications) - if err != nil { - return nil, err - } - defer rows.Close() - items := []uuid.UUID{} - for rows.Next() { - var user_id uuid.UUID - if err := rows.Scan(&user_id); err != nil { - return nil, err - } - items = append(items, user_id) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const resetApplicationReviews = `-- name: ResetApplicationReviews :exec -UPDATE applications -SET assigned_reviewer_id = NULL, - status = 'submitted', - experience_rating = NULL, - passion_rating = NULL -WHERE status NOT IN ('submitted', 'started') -` - -func (q *Queries) ResetApplicationReviews(ctx context.Context) error { - _, err := q.db.Exec(ctx, resetApplicationReviews) +func (q *Queries) ResetApplicationsToSubmitted(ctx context.Context) error { + _, err := q.db.Exec(ctx, resetApplicationsToSubmitted) return err } -const transitionAcceptedApplicationsToWaitlist = `-- name: TransitionAcceptedApplicationsToWaitlist :exec -UPDATE applications -SET waitlist_join_time = COALESCE(waitlist_join_time, NOW()), - status = 'waitlisted' -WHERE status = 'accepted' - AND user_id IN ( - SELECT id from users - WHERE role = 'applicant' -) +const searchApplicationsWithUserInfo = `-- name: SearchApplicationsWithUserInfo :many +SELECT + a.id, + a.user_id, + a.status, + a.created_at, + a.submitted_at, + a.application, + a.is_early, + u.name, + u.image, + u.email +FROM applications a +JOIN users u ON u.id = a.user_id +WHERE (LOWER(u.name) LIKE LOWER('%' || COALESCE($1, '') || '%') + OR LOWER(u.email) LIKE LOWER('%' || COALESCE($1, '') || '%')) + AND a.hackathon_id = $2 +ORDER BY a.created_at DESC +LIMIT $4 OFFSET $3 ` -func (q *Queries) TransitionAcceptedApplicationsToWaitlist(ctx context.Context) error { - _, err := q.db.Exec(ctx, transitionAcceptedApplicationsToWaitlist) - return err +type SearchApplicationsWithUserInfoParams struct { + Search *string `json:"search"` + HackathonID string `json:"hackathon_id"` + Offset int32 `json:"offset"` + Limit int32 `json:"limit"` } -const transitionWaitlistedApplicationsToAccepted = `-- name: TransitionWaitlistedApplicationsToAccepted :many -UPDATE applications -SET waitlist_join_time = NULL, - status = 'accepted' -WHERE user_id IN ( - SELECT user_id FROM applications - WHERE status = 'waitlisted' - ORDER BY waitlist_join_time ASC - LIMIT $1::int -) -RETURNING user_id -` +type SearchApplicationsWithUserInfoRow struct { + ID uuid.UUID `json:"id"` + UserID uuid.UUID `json:"user_id"` + Status ApplicationStatus `json:"status"` + CreatedAt time.Time `json:"created_at"` + SubmittedAt *time.Time `json:"submitted_at"` + Application []byte `json:"application"` + IsEarly bool `json:"is_early"` + Name string `json:"name"` + Image *string `json:"image"` + Email *string `json:"email"` +} -func (q *Queries) TransitionWaitlistedApplicationsToAccepted(ctx context.Context, acceptancecount int32) ([]uuid.UUID, error) { - rows, err := q.db.Query(ctx, transitionWaitlistedApplicationsToAccepted, acceptancecount) +func (q *Queries) SearchApplicationsWithUserInfo(ctx context.Context, arg SearchApplicationsWithUserInfoParams) ([]SearchApplicationsWithUserInfoRow, error) { + rows, err := q.db.Query(ctx, searchApplicationsWithUserInfo, + arg.Search, + arg.HackathonID, + arg.Offset, + arg.Limit, + ) if err != nil { return nil, err } defer rows.Close() - items := []uuid.UUID{} + items := []SearchApplicationsWithUserInfoRow{} for rows.Next() { - var user_id uuid.UUID - if err := rows.Scan(&user_id); err != nil { + var i SearchApplicationsWithUserInfoRow + if err := rows.Scan( + &i.ID, + &i.UserID, + &i.Status, + &i.CreatedAt, + &i.SubmittedAt, + &i.Application, + &i.IsEarly, + &i.Name, + &i.Image, + &i.Email, + ); err != nil { return nil, err } - items = append(items, user_id) + items = append(items, i) } if err := rows.Err(); err != nil { return nil, err @@ -311,43 +397,77 @@ func (q *Queries) TransitionWaitlistedApplicationsToAccepted(ctx context.Context return items, nil } -const updateApplication = `-- name: UpdateApplication :exec +const updateApplicationById = `-- name: UpdateApplicationById :exec UPDATE applications SET status = CASE WHEN $1::boolean THEN $2::application_status ELSE status END, application = CASE WHEN $3::boolean THEN $4::JSONB ELSE application END, submitted_at = CASE WHEN $5::boolean THEN $6::timestamptz ELSE submitted_at END, saved_at = CASE WHEN $7::boolean THEN $8::timestamptz ELSE saved_at END, - assigned_reviewer_id = CASE WHEN $9::boolean THEN $10::UUID ELSE assigned_reviewer_id END, - experience_rating = CASE WHEN $11::boolean THEN $12::INT ELSE experience_rating END, - passion_rating = CASE WHEN $13::boolean THEN $14::INT ELSE passion_rating END, - is_early = CASE WHEN $15::boolean THEN $16::boolean ELSE is_early END + is_early = CASE WHEN $9::boolean THEN $10::boolean ELSE is_early END WHERE - user_id = $17 + id = $11 ` -type UpdateApplicationParams struct { - StatusDoUpdate bool `json:"status_do_update"` - Status ApplicationStatus `json:"status"` - ApplicationDoUpdate bool `json:"application_do_update"` - Application []byte `json:"application"` - SubmittedAtDoUpdate bool `json:"submitted_at_do_update"` - SubmittedAt time.Time `json:"submitted_at"` - SavedAtDoUpdate bool `json:"saved_at_do_update"` - SavedAt time.Time `json:"saved_at"` - AssignedReviewerIDDoUpdate bool `json:"assigned_reviewer_id_do_update"` - AssignedReviewerID uuid.UUID `json:"assigned_reviewer_id"` - ExperienceRatingDoUpdate bool `json:"experience_rating_do_update"` - ExperienceRating int32 `json:"experience_rating"` - PassionRatingDoUpdate bool `json:"passion_rating_do_update"` - PassionRating int32 `json:"passion_rating"` - IsEarlyDoUpdate bool `json:"is_early_do_update"` - IsEarly bool `json:"is_early"` - UserID uuid.UUID `json:"user_id"` +type UpdateApplicationByIdParams struct { + StatusDoUpdate bool `json:"status_do_update"` + Status ApplicationStatus `json:"status"` + ApplicationDoUpdate bool `json:"application_do_update"` + Application []byte `json:"application"` + SubmittedAtDoUpdate bool `json:"submitted_at_do_update"` + SubmittedAt time.Time `json:"submitted_at"` + SavedAtDoUpdate bool `json:"saved_at_do_update"` + SavedAt time.Time `json:"saved_at"` + IsEarlyDoUpdate bool `json:"is_early_do_update"` + IsEarly bool `json:"is_early"` + ID uuid.UUID `json:"id"` } -func (q *Queries) UpdateApplication(ctx context.Context, arg UpdateApplicationParams) error { - _, err := q.db.Exec(ctx, updateApplication, +func (q *Queries) UpdateApplicationById(ctx context.Context, arg UpdateApplicationByIdParams) error { + _, err := q.db.Exec(ctx, updateApplicationById, + arg.StatusDoUpdate, + arg.Status, + arg.ApplicationDoUpdate, + arg.Application, + arg.SubmittedAtDoUpdate, + arg.SubmittedAt, + arg.SavedAtDoUpdate, + arg.SavedAt, + arg.IsEarlyDoUpdate, + arg.IsEarly, + arg.ID, + ) + return err +} + +const updateApplicationByUserId = `-- name: UpdateApplicationByUserId :exec +UPDATE applications +SET + status = CASE WHEN $1::boolean THEN $2::application_status ELSE status END, + application = CASE WHEN $3::boolean THEN $4::JSONB ELSE application END, + submitted_at = CASE WHEN $5::boolean THEN $6::timestamptz ELSE submitted_at END, + saved_at = CASE WHEN $7::boolean THEN $8::timestamptz ELSE saved_at END, + is_early = CASE WHEN $9::boolean THEN $10::boolean ELSE is_early END +WHERE + user_id = $11 +` + +type UpdateApplicationByUserIdParams struct { + StatusDoUpdate bool `json:"status_do_update"` + Status ApplicationStatus `json:"status"` + ApplicationDoUpdate bool `json:"application_do_update"` + Application []byte `json:"application"` + SubmittedAtDoUpdate bool `json:"submitted_at_do_update"` + SubmittedAt time.Time `json:"submitted_at"` + SavedAtDoUpdate bool `json:"saved_at_do_update"` + SavedAt time.Time `json:"saved_at"` + IsEarlyDoUpdate bool `json:"is_early_do_update"` + IsEarly bool `json:"is_early"` + UserID uuid.UUID `json:"user_id"` +} + +func (q *Queries) UpdateApplicationByUserId(ctx context.Context, arg UpdateApplicationByUserIdParams) error { + _, err := q.db.Exec(ctx, updateApplicationByUserId, arg.StatusDoUpdate, arg.Status, arg.ApplicationDoUpdate, @@ -356,12 +476,6 @@ func (q *Queries) UpdateApplication(ctx context.Context, arg UpdateApplicationPa arg.SubmittedAt, arg.SavedAtDoUpdate, arg.SavedAt, - arg.AssignedReviewerIDDoUpdate, - arg.AssignedReviewerID, - arg.ExperienceRatingDoUpdate, - arg.ExperienceRating, - arg.PassionRatingDoUpdate, - arg.PassionRating, arg.IsEarlyDoUpdate, arg.IsEarly, arg.UserID, @@ -369,18 +483,46 @@ func (q *Queries) UpdateApplication(ctx context.Context, arg UpdateApplicationPa return err } -const updateApplicationStatus = `-- name: UpdateApplicationStatus :exec +const updateApplicationsStatusByIds = `-- name: UpdateApplicationsStatusByIds :exec UPDATE applications SET status = $1::application_status -WHERE user_id = ANY($2::uuid[]) +WHERE id = ANY($2::uuid[]) +` + +type UpdateApplicationsStatusByIdsParams struct { + Status ApplicationStatus `json:"status"` + Ids []uuid.UUID `json:"ids"` +} + +func (q *Queries) UpdateApplicationsStatusByIds(ctx context.Context, arg UpdateApplicationsStatusByIdsParams) error { + _, err := q.db.Exec(ctx, updateApplicationsStatusByIds, arg.Status, arg.Ids) + return err +} + +const waitlistAcceptedApplications = `-- name: WaitlistAcceptedApplications :exec +UPDATE applications +SET waitlist_join_time = COALESCE(waitlist_join_time, NOW()), + status = 'waitlisted' +WHERE status = 'accepted' + AND user_id IN ( + SELECT id from users + WHERE role = 'applicant' +) ` -type UpdateApplicationStatusParams struct { - Status ApplicationStatus `json:"status"` - UserIds []uuid.UUID `json:"user_ids"` +func (q *Queries) WaitlistAcceptedApplications(ctx context.Context) error { + _, err := q.db.Exec(ctx, waitlistAcceptedApplications) + return err } -func (q *Queries) UpdateApplicationStatus(ctx context.Context, arg UpdateApplicationStatusParams) error { - _, err := q.db.Exec(ctx, updateApplicationStatus, arg.Status, arg.UserIds) +const waitlistApplicationById = `-- name: WaitlistApplicationById :exec +UPDATE applications +SET waitlist_join_time = COALESCE(waitlist_join_time, NOW()), + status = 'waitlisted' +WHERE id = $1 +` + +func (q *Queries) WaitlistApplicationById(ctx context.Context, id uuid.UUID) error { + _, err := q.db.Exec(ctx, waitlistApplicationById, id) return err } diff --git a/apps/api/internal/database/sqlc/email_campaigns.sql.go b/apps/api/internal/database/sqlc/email_campaigns.sql.go index c18a9414..cdf9999d 100644 --- a/apps/api/internal/database/sqlc/email_campaigns.sql.go +++ b/apps/api/internal/database/sqlc/email_campaigns.sql.go @@ -31,7 +31,7 @@ INSERT INTO email_campaigns ( $4, $5, $6::email_campaign_format, - $7::email_recipient_type[], + $7::text[]::email_recipient_type[], $8, $9, $10 @@ -40,16 +40,16 @@ RETURNING id, hackathon_id, title, description, subject, body, format, recipient ` type CreateEmailCampaignParams struct { - HackathonID string `json:"hackathon_id"` - Title string `json:"title"` - Description *string `json:"description"` - Subject string `json:"subject"` - Body string `json:"body"` - Format EmailCampaignFormat `json:"format"` - RecipientTypes []EmailRecipientType `json:"recipient_types"` - ScheduledAt *time.Time `json:"scheduled_at"` - CreatedByUserID *uuid.UUID `json:"created_by_user_id"` - UpdatedByUserID *uuid.UUID `json:"updated_by_user_id"` + HackathonID string `json:"hackathon_id"` + Title string `json:"title"` + Description *string `json:"description"` + Subject string `json:"subject"` + Body string `json:"body"` + Format EmailCampaignFormat `json:"format"` + RecipientTypes []string `json:"recipient_types"` + ScheduledAt *time.Time `json:"scheduled_at"` + CreatedByUserID *uuid.UUID `json:"created_by_user_id"` + UpdatedByUserID *uuid.UUID `json:"updated_by_user_id"` } // creates a draft campaign. It stores the title, subject, body, format, recipient groups, and optional schedule time. @@ -195,7 +195,7 @@ SET ELSE format END, recipient_types = CASE WHEN $11::boolean - THEN $12::email_recipient_type[] + THEN $12::text[]::email_recipient_type[] ELSE recipient_types END, scheduled_at = CASE WHEN $13::boolean @@ -211,24 +211,24 @@ RETURNING id, hackathon_id, title, description, subject, body, format, recipient ` type UpdateEmailCampaignParams struct { - TitleDoUpdate bool `json:"title_do_update"` - Title string `json:"title"` - DescriptionDoUpdate bool `json:"description_do_update"` - Description *string `json:"description"` - SubjectDoUpdate bool `json:"subject_do_update"` - Subject string `json:"subject"` - BodyDoUpdate bool `json:"body_do_update"` - Body string `json:"body"` - FormatDoUpdate bool `json:"format_do_update"` - Format EmailCampaignFormat `json:"format"` - RecipientTypesDoUpdate bool `json:"recipient_types_do_update"` - RecipientTypes []EmailRecipientType `json:"recipient_types"` - ScheduledAtDoUpdate bool `json:"scheduled_at_do_update"` - ScheduledAt *time.Time `json:"scheduled_at"` - UpdatedByUserIDDoUpdate bool `json:"updated_by_user_id_do_update"` - UpdatedByUserID uuid.UUID `json:"updated_by_user_id"` - ID uuid.UUID `json:"id"` - HackathonID string `json:"hackathon_id"` + TitleDoUpdate bool `json:"title_do_update"` + Title string `json:"title"` + DescriptionDoUpdate bool `json:"description_do_update"` + Description *string `json:"description"` + SubjectDoUpdate bool `json:"subject_do_update"` + Subject string `json:"subject"` + BodyDoUpdate bool `json:"body_do_update"` + Body string `json:"body"` + FormatDoUpdate bool `json:"format_do_update"` + Format NullEmailCampaignFormat `json:"format"` + RecipientTypesDoUpdate bool `json:"recipient_types_do_update"` + RecipientTypes []string `json:"recipient_types"` + ScheduledAtDoUpdate bool `json:"scheduled_at_do_update"` + ScheduledAt *time.Time `json:"scheduled_at"` + UpdatedByUserIDDoUpdate bool `json:"updated_by_user_id_do_update"` + UpdatedByUserID uuid.UUID `json:"updated_by_user_id"` + ID uuid.UUID `json:"id"` + HackathonID string `json:"hackathon_id"` } // edits draft-like campaign fields: title, description, subject, body, format, recipients, and scheduled time. diff --git a/apps/api/internal/database/sqlc/hackathons.sql.go b/apps/api/internal/database/sqlc/hackathons.sql.go index 3a84b299..f079a977 100644 --- a/apps/api/internal/database/sqlc/hackathons.sql.go +++ b/apps/api/internal/database/sqlc/hackathons.sql.go @@ -217,7 +217,7 @@ func (q *Queries) GetHackathon(ctx context.Context) (Hackathon, error) { } const getStaff = `-- name: GetStaff :many -SELECT id, name, email, email_verified, onboarded, image, created_at, updated_at, preferred_email, email_consent, checked_in_at, rfid, role_assigned_at, role FROM users +SELECT id, name, email, email_verified, onboarded, image, created_at, updated_at, preferred_email, email_consent, checked_in_at, rfid, role_assigned_at, role, has_seen_new_application_status FROM users WHERE role IN ('admin', 'staff') ` @@ -245,6 +245,7 @@ func (q *Queries) GetStaff(ctx context.Context) ([]User, error) { &i.Rfid, &i.RoleAssignedAt, &i.Role, + &i.HasSeenNewApplicationStatus, ); err != nil { return nil, err } @@ -271,7 +272,8 @@ SET start_time = CASE WHEN $19::boolean THEN $20 ELSE start_time END, end_time = CASE WHEN $21::boolean THEN $22 ELSE end_time END, banner = CASE WHEN $23::boolean THEN $24 ELSE banner END, - application_review_started = CASE WHEN $25::boolean THEN $26 ELSE application_review_started END + application_review_started = CASE WHEN $25::boolean THEN $26 ELSE application_review_started END, + updated_at = NOW() WHERE is_active = true RETURNING id, name, description, location, location_url, max_attendees, application_open, application_close, rsvp_deadline, decision_release, start_time, end_time, is_active, created_at, updated_at, banner, application_review_started, accept_early_applications, early_application_open, early_application_close ` diff --git a/apps/api/internal/database/sqlc/models.go b/apps/api/internal/database/sqlc/models.go index 58fa9d52..159390ca 100644 --- a/apps/api/internal/database/sqlc/models.go +++ b/apps/api/internal/database/sqlc/models.go @@ -12,6 +12,48 @@ import ( "github.com/google/uuid" ) +type ApplicationAutoDecisionType string + +const ( + ApplicationAutoDecisionTypeAutoAccept ApplicationAutoDecisionType = "auto_accept" + ApplicationAutoDecisionTypeAutoReject ApplicationAutoDecisionType = "auto_reject" +) + +func (e *ApplicationAutoDecisionType) Scan(src interface{}) error { + switch s := src.(type) { + case []byte: + *e = ApplicationAutoDecisionType(s) + case string: + *e = ApplicationAutoDecisionType(s) + default: + return fmt.Errorf("unsupported scan type for ApplicationAutoDecisionType: %T", src) + } + return nil +} + +type NullApplicationAutoDecisionType struct { + ApplicationAutoDecisionType ApplicationAutoDecisionType `json:"application_auto_decision_type"` + Valid bool `json:"valid"` // Valid is true if ApplicationAutoDecisionType is not NULL +} + +// Scan implements the Scanner interface. +func (ns *NullApplicationAutoDecisionType) Scan(value interface{}) error { + if value == nil { + ns.ApplicationAutoDecisionType, ns.Valid = "", false + return nil + } + ns.Valid = true + return ns.ApplicationAutoDecisionType.Scan(value) +} + +// Value implements the driver Valuer interface. +func (ns NullApplicationAutoDecisionType) Value() (driver.Value, error) { + if !ns.Valid { + return nil, nil + } + return string(ns.ApplicationAutoDecisionType), nil +} + type ApplicationStatus string const ( @@ -22,6 +64,7 @@ const ( ApplicationStatusRejected ApplicationStatus = "rejected" ApplicationStatusWaitlisted ApplicationStatus = "waitlisted" ApplicationStatusWithdrawn ApplicationStatus = "withdrawn" + ApplicationStatusConfirmed ApplicationStatus = "confirmed" ) func (e *ApplicationStatus) Scan(src interface{}) error { @@ -385,19 +428,40 @@ type Account struct { } type Application struct { - UserID uuid.UUID `json:"user_id"` - Status ApplicationStatus `json:"status"` - Application []byte `json:"application"` - CreatedAt time.Time `json:"created_at"` - SavedAt time.Time `json:"saved_at"` - UpdatedAt time.Time `json:"updated_at"` - SubmittedAt *time.Time `json:"submitted_at"` - ExperienceRating *int32 `json:"experience_rating"` - PassionRating *int32 `json:"passion_rating"` - AssignedReviewerID *uuid.UUID `json:"assigned_reviewer_id"` - WaitlistJoinTime *time.Time `json:"waitlist_join_time"` - HackathonID string `json:"hackathon_id"` - IsEarly bool `json:"is_early"` + UserID uuid.UUID `json:"user_id"` + Status ApplicationStatus `json:"status"` + Application []byte `json:"application"` + CreatedAt time.Time `json:"created_at"` + SavedAt time.Time `json:"saved_at"` + UpdatedAt time.Time `json:"updated_at"` + SubmittedAt *time.Time `json:"submitted_at"` + HackathonID string `json:"hackathon_id"` + IsEarly bool `json:"is_early"` + ID uuid.UUID `json:"id"` +} + +type ApplicationAutoDecisionRequest struct { + ID uuid.UUID `json:"id"` + ApplicationID uuid.UUID `json:"application_id"` + ReviewerID uuid.UUID `json:"reviewer_id"` + RequestedDecision ApplicationAutoDecisionType `json:"requested_decision"` + Justification *string `json:"justification"` + Approved *bool `json:"approved"` + DecidedBy *uuid.UUID `json:"decided_by"` + UpdatedAt time.Time `json:"updated_at"` + CreatedAt time.Time `json:"created_at"` +} + +type ApplicationReview struct { + ID uuid.UUID `json:"id"` + ApplicationID uuid.UUID `json:"application_id"` + ReviewerID uuid.UUID `json:"reviewer_id"` + ExperienceRating *int32 `json:"experience_rating"` + PassionRating *int32 `json:"passion_rating"` + Notes *string `json:"notes"` + UpdatedBy *uuid.UUID `json:"updated_by"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } type BatRun struct { @@ -521,20 +585,21 @@ type TeamMember struct { } type User struct { - ID uuid.UUID `json:"id"` - Name string `json:"name"` - Email *string `json:"email"` - EmailVerified bool `json:"email_verified"` - Onboarded bool `json:"onboarded"` - Image *string `json:"image"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - PreferredEmail *string `json:"preferred_email"` - EmailConsent bool `json:"email_consent"` - CheckedInAt *time.Time `json:"checked_in_at"` - Rfid *string `json:"rfid"` - RoleAssignedAt *time.Time `json:"role_assigned_at"` - Role UserRole `json:"role"` + ID uuid.UUID `json:"id"` + Name string `json:"name"` + Email *string `json:"email"` + EmailVerified bool `json:"email_verified"` + Onboarded bool `json:"onboarded"` + Image *string `json:"image"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + PreferredEmail *string `json:"preferred_email"` + EmailConsent bool `json:"email_consent"` + CheckedInAt *time.Time `json:"checked_in_at"` + Rfid *string `json:"rfid"` + RoleAssignedAt *time.Time `json:"role_assigned_at"` + Role UserRole `json:"role"` + HasSeenNewApplicationStatus *bool `json:"has_seen_new_application_status"` } type UserRedemption struct { diff --git a/apps/api/internal/database/sqlc/sessions.sql.go b/apps/api/internal/database/sqlc/sessions.sql.go index ea2f8f63..54d16a91 100644 --- a/apps/api/internal/database/sqlc/sessions.sql.go +++ b/apps/api/internal/database/sqlc/sessions.sql.go @@ -57,7 +57,10 @@ func (q *Queries) DeleteExpiredSession(ctx context.Context) error { } const getActiveSessionUserInfo = `-- name: GetActiveSessionUserInfo :one -SELECT u.id AS user_id, u.name, u.email, u.preferred_email, u.onboarded, u.image, u.role, u.email_consent, u.checked_in_at, u.rfid, s.last_used_at +SELECT u.id AS user_id, u.name, u.email, u.preferred_email, + u.onboarded, u.image, u.role, u.email_consent, + u.checked_in_at, u.rfid, u.has_seen_new_application_status, + s.last_used_at FROM sessions s JOIN users u ON s.user_id = u.id WHERE s.id = $1 @@ -66,17 +69,18 @@ LIMIT 1 ` type GetActiveSessionUserInfoRow struct { - UserID uuid.UUID `json:"user_id"` - Name string `json:"name"` - Email *string `json:"email"` - PreferredEmail *string `json:"preferred_email"` - Onboarded bool `json:"onboarded"` - Image *string `json:"image"` - Role UserRole `json:"role"` - EmailConsent bool `json:"email_consent"` - CheckedInAt *time.Time `json:"checked_in_at"` - Rfid *string `json:"rfid"` - LastUsedAt time.Time `json:"last_used_at"` + UserID uuid.UUID `json:"user_id"` + Name string `json:"name"` + Email *string `json:"email"` + PreferredEmail *string `json:"preferred_email"` + Onboarded bool `json:"onboarded"` + Image *string `json:"image"` + Role UserRole `json:"role"` + EmailConsent bool `json:"email_consent"` + CheckedInAt *time.Time `json:"checked_in_at"` + Rfid *string `json:"rfid"` + HasSeenNewApplicationStatus *bool `json:"has_seen_new_application_status"` + LastUsedAt time.Time `json:"last_used_at"` } func (q *Queries) GetActiveSessionUserInfo(ctx context.Context, id uuid.UUID) (GetActiveSessionUserInfoRow, error) { @@ -93,6 +97,7 @@ func (q *Queries) GetActiveSessionUserInfo(ctx context.Context, id uuid.UUID) (G &i.EmailConsent, &i.CheckedInAt, &i.Rfid, + &i.HasSeenNewApplicationStatus, &i.LastUsedAt, ) return i, err diff --git a/apps/api/internal/database/sqlc/users.sql.go b/apps/api/internal/database/sqlc/users.sql.go index 489ed1d1..1c2b5a6c 100644 --- a/apps/api/internal/database/sqlc/users.sql.go +++ b/apps/api/internal/database/sqlc/users.sql.go @@ -15,7 +15,7 @@ import ( const createUser = `-- name: CreateUser :one INSERT INTO users (name, email, image) VALUES ($1, $2, $3) -RETURNING id, name, email, email_verified, onboarded, image, created_at, updated_at, preferred_email, email_consent, checked_in_at, rfid, role_assigned_at, role +RETURNING id, name, email, email_verified, onboarded, image, created_at, updated_at, preferred_email, email_consent, checked_in_at, rfid, role_assigned_at, role, has_seen_new_application_status ` type CreateUserParams struct { @@ -42,6 +42,7 @@ func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, e &i.Rfid, &i.RoleAssignedAt, &i.Role, + &i.HasSeenNewApplicationStatus, ) return i, err } @@ -57,7 +58,7 @@ func (q *Queries) DeleteUser(ctx context.Context, id uuid.UUID) error { } const getUserByEmail = `-- name: GetUserByEmail :one -SELECT id, name, email, email_verified, onboarded, image, created_at, updated_at, preferred_email, email_consent, checked_in_at, rfid, role_assigned_at, role FROM users +SELECT id, name, email, email_verified, onboarded, image, created_at, updated_at, preferred_email, email_consent, checked_in_at, rfid, role_assigned_at, role, has_seen_new_application_status FROM users WHERE email = $1 ` @@ -79,12 +80,13 @@ func (q *Queries) GetUserByEmail(ctx context.Context, email *string) (User, erro &i.Rfid, &i.RoleAssignedAt, &i.Role, + &i.HasSeenNewApplicationStatus, ) return i, err } const getUserByID = `-- name: GetUserByID :one -SELECT id, name, email, email_verified, onboarded, image, created_at, updated_at, preferred_email, email_consent, checked_in_at, rfid, role_assigned_at, role FROM users +SELECT id, name, email, email_verified, onboarded, image, created_at, updated_at, preferred_email, email_consent, checked_in_at, rfid, role_assigned_at, role, has_seen_new_application_status FROM users WHERE id = $1 ` @@ -106,12 +108,13 @@ func (q *Queries) GetUserByID(ctx context.Context, id uuid.UUID) (User, error) { &i.Rfid, &i.RoleAssignedAt, &i.Role, + &i.HasSeenNewApplicationStatus, ) return i, err } const getUserByRFID = `-- name: GetUserByRFID :one -SELECT id, name, email, email_verified, onboarded, image, created_at, updated_at, preferred_email, email_consent, checked_in_at, rfid, role_assigned_at, role FROM users +SELECT id, name, email, email_verified, onboarded, image, created_at, updated_at, preferred_email, email_consent, checked_in_at, rfid, role_assigned_at, role, has_seen_new_application_status FROM users WHERE rfid = $1 ` @@ -133,6 +136,7 @@ func (q *Queries) GetUserByRFID(ctx context.Context, rfid *string) (User, error) &i.Rfid, &i.RoleAssignedAt, &i.Role, + &i.HasSeenNewApplicationStatus, ) return i, err } @@ -170,7 +174,7 @@ func (q *Queries) GetUserEmailInfoById(ctx context.Context, id uuid.UUID) (GetUs } const getUsers = `-- name: GetUsers :many -SELECT id, name, email, email_verified, onboarded, image, created_at, updated_at, preferred_email, email_consent, checked_in_at, rfid, role_assigned_at, role +SELECT id, name, email, email_verified, onboarded, image, created_at, updated_at, preferred_email, email_consent, checked_in_at, rfid, role_assigned_at, role, has_seen_new_application_status FROM users WHERE LOWER(name) LIKE LOWER('%' || COALESCE($1, '') || '%') OR LOWER(email) LIKE LOWER('%' || COALESCE($1, '') || '%') @@ -208,6 +212,7 @@ func (q *Queries) GetUsers(ctx context.Context, arg GetUsersParams) ([]User, err &i.Rfid, &i.RoleAssignedAt, &i.Role, + &i.HasSeenNewApplicationStatus, ); err != nil { return nil, err } @@ -292,35 +297,36 @@ SET email_consent = CASE WHEN $13::boolean THEN $14 ELSE email_consent END, checked_in_at = CASE WHEN $15::boolean THEN $16 ELSE checked_in_at END, rfid = CASE WHEN $17::boolean THEN $18 ELSE rfid END, - role = CASE WHEN $19::boolean THEN $20 ELSE role END, - role_assigned_at = CASE WHEN $19::boolean THEN NOW() ELSE role_assigned_at END, + -- role = CASE WHEN @role_do_update::boolean THEN @role ELSE role END, + -- role_assigned_at = CASE WHEN @role_do_update::boolean THEN NOW() ELSE role_assigned_at END, + has_seen_new_application_status = CASE WHEN $19::boolean THEN $20 ELSE has_seen_new_application_status END, updated_at = NOW() WHERE id = $21::uuid ` type UpdateUserParams struct { - NameDoUpdate bool `json:"name_do_update"` - Name string `json:"name"` - EmailDoUpdate bool `json:"email_do_update"` - Email *string `json:"email"` - EmailVerifiedDoUpdate bool `json:"email_verified_do_update"` - EmailVerified bool `json:"email_verified"` - PreferredEmailDoUpdate bool `json:"preferred_email_do_update"` - PreferredEmail *string `json:"preferred_email"` - OnboardedDoUpdate bool `json:"onboarded_do_update"` - Onboarded bool `json:"onboarded"` - ImageDoUpdate bool `json:"image_do_update"` - Image *string `json:"image"` - EmailConsentDoUpdate bool `json:"email_consent_do_update"` - EmailConsent bool `json:"email_consent"` - CheckedInAtDoUpdate bool `json:"checked_in_at_do_update"` - CheckedInAt *time.Time `json:"checked_in_at"` - RfidDoUpdate bool `json:"rfid_do_update"` - Rfid *string `json:"rfid"` - RoleDoUpdate bool `json:"role_do_update"` - Role UserRole `json:"role"` - ID uuid.UUID `json:"id"` + NameDoUpdate bool `json:"name_do_update"` + Name string `json:"name"` + EmailDoUpdate bool `json:"email_do_update"` + Email *string `json:"email"` + EmailVerifiedDoUpdate bool `json:"email_verified_do_update"` + EmailVerified bool `json:"email_verified"` + PreferredEmailDoUpdate bool `json:"preferred_email_do_update"` + PreferredEmail *string `json:"preferred_email"` + OnboardedDoUpdate bool `json:"onboarded_do_update"` + Onboarded bool `json:"onboarded"` + ImageDoUpdate bool `json:"image_do_update"` + Image *string `json:"image"` + EmailConsentDoUpdate bool `json:"email_consent_do_update"` + EmailConsent bool `json:"email_consent"` + CheckedInAtDoUpdate bool `json:"checked_in_at_do_update"` + CheckedInAt *time.Time `json:"checked_in_at"` + RfidDoUpdate bool `json:"rfid_do_update"` + Rfid *string `json:"rfid"` + HasSeenNewApplicationStatusDoUpdate bool `json:"has_seen_new_application_status_do_update"` + HasSeenNewApplicationStatus *bool `json:"has_seen_new_application_status"` + ID uuid.UUID `json:"id"` } func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) error { @@ -343,8 +349,8 @@ func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) error { arg.CheckedInAt, arg.RfidDoUpdate, arg.Rfid, - arg.RoleDoUpdate, - arg.Role, + arg.HasSeenNewApplicationStatusDoUpdate, + arg.HasSeenNewApplicationStatus, arg.ID, ) return err diff --git a/apps/api/internal/domains/application/handlers.go b/apps/api/internal/domains/application/handlers.go new file mode 100644 index 00000000..785d9a4f --- /dev/null +++ b/apps/api/internal/domains/application/handlers.go @@ -0,0 +1,1002 @@ +package application + +import ( + "bytes" + "context" + "errors" + "io" + "net/http" + "strconv" + + "github.com/danielgtaylor/huma/v2" + "github.com/go-playground/validator/v10" + "github.com/google/uuid" + "github.com/swamphacks/core/apps/api/internal/api/middleware" + "github.com/swamphacks/core/apps/api/internal/ctxutils" + "github.com/swamphacks/core/apps/api/internal/database" + "github.com/swamphacks/core/apps/api/internal/database/sqlc" +) + +type GetMyApplicationOutput struct { + Body MyApplicationResponseDto +} + +func (h *handler) handleGetMyApplication(ctx context.Context, input *struct{}) (*GetMyApplicationOutput, error) { + userCtx := ctxutils.GetUserFromCtx(ctx) + + if userCtx == nil { + return nil, huma.Error400BadRequest("Failed to get current user info") + } + + application, err := h.applicationService.GetApplicationByUserId(ctx, userCtx.UserID) + + if err != nil { + if errors.Is(err, database.ErrApplicationNotFound) { + newApplication, err := h.applicationService.CreateApplication(ctx, userCtx.UserID) + + if err != nil || newApplication == nil { + return nil, huma.Error500InternalServerError(err.Error()) + } + + return &GetMyApplicationOutput{Body: MyApplicationResponseDto{ + ID: newApplication.ID, + UserID: newApplication.UserID, + Status: string(newApplication.Status), + Application: newApplication.Application, + CreatedAt: newApplication.CreatedAt, + SavedAt: newApplication.SavedAt, + UpdatedAt: newApplication.UpdatedAt, + SubmittedAt: newApplication.SubmittedAt, + HackathonID: newApplication.HackathonID, + }}, nil + } + + if errors.Is(err, ErrApplicationNotOpened) { + return nil, huma.Error400BadRequest(err.Error()) + } + + return nil, huma.Error500InternalServerError("error retrieving application") + } + + if application == nil { + return nil, huma.Error500InternalServerError("Application is null") + } + + return &GetMyApplicationOutput{Body: MyApplicationResponseDto{ + ID: application.ID, + UserID: application.UserID, + Status: string(application.Status), + Application: application.Application, + CreatedAt: application.CreatedAt, + SavedAt: application.SavedAt, + UpdatedAt: application.UpdatedAt, + SubmittedAt: application.SubmittedAt, + HackathonID: application.HackathonID, + }}, nil +} + +type UpdateApplicationByIdOutput struct { + Status int +} + +func (h *handler) handleUpdateApplicationById(ctx context.Context, input *struct { + Body UpdateApplicationRequestDto +}) (*UpdateApplicationByIdOutput, error) { + err := h.applicationService.UpdateApplicationById(ctx, input.Body) + + if err != nil { + return nil, huma.Error500InternalServerError("error updating application") + } + + return &UpdateApplicationByIdOutput{Status: http.StatusNoContent}, nil +} + +type GetExtendedApplicationOutput struct { + Body ExtendedApplicationResponseDto +} + +func (h *handler) handleGetExtendedApplicationById(ctx context.Context, input *struct { + ID uuid.UUID `path:"applicationId"` +}) (*GetExtendedApplicationOutput, error) { + application, err := h.applicationService.GetExtendedApplicationById(ctx, input.ID) + + if err != nil { + return nil, huma.Error500InternalServerError("error retrieving extended application") + } + + resumeRequest, err := h.applicationService.GetApplicationResumeURL(ctx, application.UserID, 600) + + if err != nil { + h.logger.Err(err).Str("ApplicationId", input.ID.String()).Msg(err.Error()) + return nil, huma.Error500InternalServerError("unable to get application resume") + } + + var review *ReviewDto + if application.ReviewID != nil { + review = &ReviewDto{ + ID: *application.ReviewID, + ExperienceRating: application.ExperienceRating, + PassionRating: application.PassionRating, + Notes: application.Notes, + ReviewUpdatedAt: *application.ReviewUpdatedAt, + ReviewUpdatedBy: application.ReviewUpdatedBy, + Reviewer: AppUser{ + ID: *application.ReviewerID, + UserName: *application.ReviewerName, + Image: application.ReviewerImage, + }, + } + } + + var autoDecisionRequest *AutoDecisionRequestDto + if application.AutoDecisionRequestID != nil && application.RequestedDecision.Valid { + autoDecisionRequest = &AutoDecisionRequestDto{ + ID: *application.AutoDecisionRequestID, + ApplicationID: application.ID, + RequestedDecision: string(application.RequestedDecision.ApplicationAutoDecisionType), + Justification: application.DecisionJustification, + AutoDecisionApproved: *application.DecisionApproved, + CreatedAt: *application.DecisionRequestCreatedAt, + DecidedBy: application.DecidedBy, + } + } + + return &GetExtendedApplicationOutput{Body: ExtendedApplicationResponseDto{ + ID: application.ID, + User: AppUser{ + ID: application.UserID, + UserName: application.UserName, + Image: application.UserImage, + Email: application.UserEmail, + }, + Status: string(application.Status), + Application: application.Application, + CreatedAt: application.CreatedAt, + UpdatedAt: application.UpdatedAt, + SubmittedAt: application.SubmittedAt, + IsEarly: application.IsEarly, + Review: review, + AutoDecisionRequest: autoDecisionRequest, + ResumeURL: resumeRequest.URL, + }}, nil +} + +type SearchApplicationsOutput struct { + Body SearchApplicationsResponseDto +} + +func (h *handler) handleSearchApplications(ctx context.Context, input *struct { + Limit int32 `query:"limit"` + Offset int32 `query:"offset"` + Search string `query:"search"` +}) (*SearchApplicationsOutput, error) { + count, applications, err := h.applicationService.SearchApplications(ctx, input.Limit, input.Offset, input.Search) + + if err != nil { + return nil, huma.Error500InternalServerError("error retrieving applications") + } + + searchApplicationResponses := make([]ApplicationWithUserInfoDto, len(applications)) + + for i, val := range applications { + searchApplicationResponses[i] = ApplicationWithUserInfoDto{ + ID: val.ID, + User: AppUser{ + ID: val.UserID, + UserName: val.Name, + Image: val.Image, + Email: val.Email, + }, + Status: string(val.Status), + Application: val.Application, + CreatedAt: val.CreatedAt, + SubmittedAt: val.SubmittedAt, + UpdatedAt: val.CreatedAt, + IsEarly: val.IsEarly, + } + } + + return &SearchApplicationsOutput{Body: SearchApplicationsResponseDto{ + Count: *count, + Applications: searchApplicationResponses, + }}, nil +} + +type SaveApplicationOutput struct { + Status int +} + +func (h *handler) handleSaveApplication(ctx context.Context, input *struct { + Body any +}) (*SaveApplicationOutput, error) { + userCtx := ctxutils.GetUserFromCtx(ctx) + + if userCtx == nil { + return nil, huma.Error400BadRequest("Failed to get current user info") + } + + // TODO: validate input.Body, make sure that the data is the application + err := h.applicationService.SaveApplication(ctx, input.Body, userCtx.UserID) + + if err != nil { + return nil, huma.Error500InternalServerError(err.Error()) + } + + return &SaveApplicationOutput{Status: http.StatusOK}, nil +} + +type SubmitApplicationOutput struct { + Body SubmitApplicationResponseDto +} + +func (h *handler) handleSubmitApplication(ctx context.Context, input *struct{}) (*SubmitApplicationOutput, error) { + userCtx := ctxutils.GetUserFromCtx(ctx) + + if userCtx == nil { + return nil, huma.Error400BadRequest("Failed to get current user info") + } + + // TODO: refactor this to using Huma's request API instead of using the raw http package + + r := ctx.Value(middleware.RawRequestKey{}).(*http.Request) + + // Parse multipart form (10 MB max memory) + err := r.ParseMultipartForm(10 << 20) + if err != nil { + return nil, huma.Error400BadRequest("Failed to parse form") + } + + var submission ApplicationSubmissionFields + + // Map form values + submission.FirstName = r.FormValue("firstName") + submission.LastName = r.FormValue("lastName") + + if ageStr := r.FormValue("age"); ageStr != "" { + if age, err := strconv.Atoi(ageStr); err == nil { + submission.Age = age + } + } + + submission.Phone = r.FormValue("phone") + submission.PreferredEmail = r.FormValue("preferredEmail") + submission.UniversityEmail = r.FormValue("universityEmail") + + submission.Country = r.FormValue("country") + submission.Gender = r.FormValue("gender") + submission.GenderOther = r.FormValue("gender-other") + submission.Pronouns = r.FormValue("pronouns") + submission.Race = r.FormValue("race") + submission.RaceOther = r.FormValue("race-other") + submission.Orientation = r.FormValue("orientation") + + submission.Linkedin = r.FormValue("linkedin") + submission.Github = r.FormValue("github") + + if ageCertStr := r.FormValue("ageCertification"); ageCertStr != "" { + submission.AgeCertification = (ageCertStr == "true" || ageCertStr == "1") + } + + submission.School = r.FormValue("school") + submission.Level = r.FormValue("level") + submission.LevelOther = r.FormValue("level-other") + submission.Year = r.FormValue("year") + submission.YearOther = r.FormValue("year-other") + submission.GraduationYear = r.FormValue("graduationYear") + submission.Majors = r.FormValue("majors") + submission.Minors = r.FormValue("minors") + submission.Experience = r.FormValue("experience") + submission.UfHackathonExp = r.FormValue("ufHackathonExp") + submission.ProjectExperience = r.FormValue("projectExperience") + submission.ShirtSize = r.FormValue("shirtSize") + submission.Diet = r.FormValue("diet") + submission.Essay1 = r.FormValue("essay1") + submission.Essay2 = r.FormValue("essay2") + submission.Referral = r.FormValue("referral") + submission.PictureConsent = r.FormValue("pictureConsent") + submission.InPersonAcknowledgement = r.FormValue("inpersonAcknowledgement") + submission.AgreeToConduct = r.FormValue("agreeToConduct") + submission.InfoShareAuthorization = r.FormValue("infoShareAuthorization") + submission.AgreeToMLHEmails = r.FormValue("agreeToMLHEmails") + + resumeFile, _, err := r.FormFile("resume[]") + if err != nil { + return nil, huma.Error400BadRequest("Invalid resume file") + } + + defer resumeFile.Close() + + resumeFileBuffer := bytes.NewBuffer(nil) + + if _, err := io.Copy(resumeFileBuffer, resumeFile); err != nil { + return nil, huma.Error500InternalServerError("Error while parsing resume") + } + + validate := validator.New() + if err := validate.Struct(submission); err != nil { + return nil, huma.Error400BadRequest("Unable to parse application submission") + } + + submittedAt, err := h.applicationService.SubmitApplication(r.Context(), submission, resumeFileBuffer.Bytes(), userCtx.UserID) + + if err != nil { + if errors.Is(err, ErrApplicationNotOpened) { + return nil, huma.Error400BadRequest("Application is not opened") + } + + return nil, huma.Error500InternalServerError("Fail to submit application") + } + + return &SubmitApplicationOutput{ + Body: SubmitApplicationResponseDto{ + SubmittedAt: submittedAt, + }, + }, nil +} + +type GetDownloadResumeOutput struct { + Body string +} + +func (h *handler) handleGetApplicationResumeURL(ctx context.Context, input *struct{}) (*GetDownloadResumeOutput, error) { + userCtx := ctxutils.GetUserFromCtx(ctx) + + if userCtx == nil { + return nil, huma.Error400BadRequest("Failed to get current user info") + } + + request, err := h.applicationService.GetApplicationResumeURL(ctx, userCtx.UserID, 60) + + if err != nil { + return nil, huma.Error500InternalServerError("Unable to get url to download resume") + } + + return &GetDownloadResumeOutput{Body: request.URL}, nil +} + +type ReplaceResumeOutput struct { + Status int +} + +func (h *handler) handleReplaceResume(ctx context.Context, input *struct { + RawBody huma.MultipartFormFiles[struct { + Resume huma.FormFile `form:"resume" contentType:"application/pdf" required:"true"` + }] +}) (*ReplaceResumeOutput, error) { + userCtx := ctxutils.GetUserFromCtx(ctx) + + if userCtx == nil { + return nil, huma.Error400BadRequest("Failed to get current user info") + } + + fileHeaders := input.RawBody.Form.File["resume"] + if len(fileHeaders) == 0 { + return nil, huma.Error400BadRequest("Invalid resume file") + } + fileHeader := fileHeaders[0] + + if fileHeader.Size > 10*1024*1024 { // 10 MiB + return nil, huma.Error400BadRequest("File too large") + } + + file, err := fileHeader.Open() + if err != nil { + return nil, huma.Error400BadRequest("Failed to parse uploaded resume") + } + defer file.Close() + + resumeBuffer := bytes.NewBuffer(nil) + if _, err := io.Copy(resumeBuffer, file); err != nil { + return nil, huma.Error500InternalServerError("Error while parsing resume") + } + + if err := h.applicationService.ReplaceResume(ctx, userCtx.UserID, resumeBuffer.Bytes()); err != nil { + if errors.Is(err, database.ErrApplicationNotFound) { + return nil, huma.Error400BadRequest("No application found to replace resume for") + } + if errors.Is(err, ErrCannotReplaceResume) { + return nil, huma.Error400BadRequest(err.Error()) + } + return nil, huma.Error500InternalServerError("Unable to replace resume") + } + + return &ReplaceResumeOutput{Status: http.StatusNoContent}, nil +} + +type GetApplicationStatisticsOutput struct { + Body ApplicationStatisticsDto +} + +func (h *handler) handleGetApplicationStatistics(ctx context.Context, input *struct{}) (*GetApplicationStatisticsOutput, error) { + stats, err := h.applicationService.GetApplicationStatistics(ctx) + + if err != nil { + return nil, huma.Error500InternalServerError("Unable to get application statistics") + } + + return &GetApplicationStatisticsOutput{Body: *stats}, nil +} + +type UpdateApplicationReviewStatusForHackathonOutput struct { + Status int +} + +func (h *handler) handleUpdateApplicationReviewStatusForHackathon(ctx context.Context, input *struct { + Body struct { + Started bool `json:"started" required:"true"` + } +}) (*UpdateApplicationReviewStatusForHackathonOutput, error) { + err := h.applicationService.UpdateApplicationReviewStatusForHackathon(ctx, input.Body.Started) + + if err != nil { + return nil, huma.Error500InternalServerError("Unable to update review status for hackathon") + } + + return &UpdateApplicationReviewStatusForHackathonOutput{Status: http.StatusOK}, nil +} + +type GetApplicationReviewDetailsOutput struct { + Body ApplicationReviewResponseDto +} + +func (h *handler) handleGetReviewById(ctx context.Context, input *struct { + ID uuid.UUID `path:"reviewId"` +}) (*GetApplicationReviewDetailsOutput, error) { + review, resume, err := h.applicationService.GetReviewById(ctx, input.ID) + + if err != nil { + return nil, huma.Error500InternalServerError("Unable to get application review details") + } + + var autoDecisionRequest *AutoDecisionRequestDto + if review.DecisionRequestID != nil && review.RequestedDecision.Valid { + autoDecisionRequest = &AutoDecisionRequestDto{ + ID: *review.DecisionRequestID, + ApplicationID: review.ID, + RequestedDecision: string(review.RequestedDecision.ApplicationAutoDecisionType), + Justification: review.DecisionJustification, + AutoDecisionApproved: *review.DecisionApproved, + CreatedAt: *review.DecisionRequestCreatedAt, + DecidedBy: review.DecisionDecidedBy, + } + } + + return &GetApplicationReviewDetailsOutput{Body: ApplicationReviewResponseDto{ + ID: review.ID, + PassionRating: review.PassionRating, + ExperienceRating: review.ExperienceRating, + Notes: review.Notes, + Application: review.Application, + ResumeURL: resume.URL, + AutoDecisionRequest: autoDecisionRequest, + }}, nil +} + +type SubmitApplicationReviewOutput struct { + Status int +} + +func (h *handler) handleSubmitApplicationReview(ctx context.Context, input *struct { + Body SaveReviewRequestDto +}) (*SubmitApplicationReviewOutput, error) { + userCtx := ctxutils.GetUserFromCtx(ctx) + + if userCtx == nil { + return nil, huma.Error400BadRequest("Failed to get current user info") + } + + err := h.applicationService.SaveApplicationReview(ctx, input.Body, userCtx.UserID, userCtx.Role) + + if err != nil { + return nil, huma.Error500InternalServerError("Unable to save review") + } + + return &SubmitApplicationReviewOutput{Status: http.StatusCreated}, nil +} + +type UpdateApplicationReviewOutput struct { + Status int +} + +func (h *handler) handleUpdateApplicationReview(ctx context.Context, input *struct { + Body UpdateReviewRequestDto +}) (*UpdateApplicationReviewOutput, error) { + userCtx := ctxutils.GetUserFromCtx(ctx) + + if userCtx == nil { + return nil, huma.Error400BadRequest("Failed to get current user info") + } + + err := h.applicationService.UpdateApplicationReview(ctx, input.Body, userCtx.UserID) + + if err != nil { + return nil, huma.Error500InternalServerError("Unable to update review") + } + + return &UpdateApplicationReviewOutput{Status: http.StatusOK}, nil +} + +type GetReviewAssignmentsOutput struct { + Body []ReviewAssignmentDto `json:"body" nullable:"false"` +} + +func (h *handler) handleGetReviewAssignments(ctx context.Context, input *struct{}) (*GetReviewAssignmentsOutput, error) { + userCtx := ctxutils.GetUserFromCtx(ctx) + + if userCtx == nil { + return nil, huma.Error400BadRequest("Failed to get current user info") + } + + reviews, err := h.applicationService.GetReviewsForReviewer(ctx, userCtx.UserID) + + if err != nil { + return nil, huma.Error500InternalServerError("Unable to get assigned applications") + } + + var assignments []ReviewAssignmentDto + for _, review := range reviews { + status := ApplicationReviewStatusInProgress + if review.ExperienceRating != nil && review.PassionRating != nil { + status = ApplicationReviewStatusCompleted + } + + assignments = append(assignments, ReviewAssignmentDto{ + ReviewID: review.ID, + UserID: review.UserID, + ApplicationID: review.ApplicationID, + Status: status, + }) + } + + return &GetReviewAssignmentsOutput{Body: assignments}, nil +} + +type AssignApplicationReviewersOutput struct { + Status int +} + +func (h *handler) handleAssignApplicationReviewers(ctx context.Context, input *struct { + Body []ReviewerAssignmentRequestDto +}) (*AssignApplicationReviewersOutput, error) { + err := h.applicationService.AssignReviewersToApplications(ctx, input.Body) + + if err != nil { + return nil, huma.Error500InternalServerError("Unable to assign reviewers") + } + + return &AssignApplicationReviewersOutput{Status: http.StatusOK}, nil +} + +type GetAllReviewersAndProgressOutput struct { + Body []ReviewerProgressResponseDto +} + +func (h *handler) handleGetAllReviewersAndProgress(ctx context.Context, input *struct{}) (*GetAllReviewersAndProgressOutput, error) { + results, err := h.applicationService.GetAllReviewersAndProgress(ctx) + + if err != nil { + return nil, huma.Error500InternalServerError("Unable to get all reviewers and their progress") + } + + reviewersProgress := make([]ReviewerProgressResponseDto, len(results)) + + for i, val := range results { + reviewersProgress[i] = ReviewerProgressResponseDto{ + ID: val.ID, + Name: val.Name, + Image: val.Image, + TotalAssigned: val.TotalAssigned, + CompletedCount: val.CompletedCount, + } + } + + return &GetAllReviewersAndProgressOutput{ + Body: reviewersProgress, + }, nil +} + +type SearchAutoDecisionRequestsOutput struct { + Body SearchAutoDecisionRequestsResponseDto +} + +func (h *handler) handleSearchAutoDecisionRequests(ctx context.Context, input *SearchAutoDecisionRequestsDto) (*SearchAutoDecisionRequestsOutput, error) { + count, requests, err := h.applicationService.SearchAutoDecisionRequests(ctx, *input) + + if err != nil { + return nil, huma.Error500InternalServerError("error retrieving requests") + } + + searchAutoDecisionRequestsResponses := make([]ExtendedAutoDecisionRequestDto, len(requests)) + + for i, val := range requests { + var decidedBy *AppUser + if val.DecidedBy != nil { + decidedBy = &AppUser{ + ID: *val.DecidedBy, + UserName: *val.ApproverName, + Image: val.ApproverImage, + } + } + + searchAutoDecisionRequestsResponses[i] = ExtendedAutoDecisionRequestDto{ + ID: val.ID, + User: AppUser{ + ID: val.UserID, + UserName: val.UserName, + Image: val.UserImage, + }, + ApplicationID: val.ApplicationID, + Reviewer: AppUser{ + ID: val.ReviewerID, + UserName: val.ReviewerName, + Image: val.ReviewerImage, + }, + DecidedBy: decidedBy, + RequestedDecision: string(val.RequestedDecision), + Justification: val.Justification, + Approved: val.Approved, + UpdatedAt: val.UpdatedAt, + CreatedAt: val.CreatedAt, + } + } + + return &SearchAutoDecisionRequestsOutput{Body: SearchAutoDecisionRequestsResponseDto{ + Count: *count, + Requests: searchAutoDecisionRequestsResponses, + }}, nil +} + +type GetAutoDecisionRequestsOutput struct { + Body []ExtendedAutoDecisionRequestDto `json:"body" nullable:"false"` +} + +func (h *handler) handleGetAutoDecisionRequests(ctx context.Context, input *struct{}) (*GetAutoDecisionRequestsOutput, error) { + requests, err := h.applicationService.GetAllAutoDecisionRequests(ctx) + + if err != nil { + return nil, huma.Error500InternalServerError("Unable to get auto decision requests") + } + + extendedRequests := make([]ExtendedAutoDecisionRequestDto, len(requests)) + + for i, val := range requests { + var decidedBy *AppUser + if val.DecidedBy != nil { + decidedBy = &AppUser{ + ID: *val.DecidedBy, + UserName: *val.ApproverName, + Image: val.ApproverImage, + } + } + + extendedRequests[i] = ExtendedAutoDecisionRequestDto{ + ID: val.ID, + ApplicationID: val.ApplicationID, + User: AppUser{ + ID: val.UserID, + UserName: val.UserName, + Image: val.UserImage, + }, + Reviewer: AppUser{ + ID: val.ReviewerID, + UserName: val.ReviewerName, + Image: val.ReviewerImage, + }, + DecidedBy: decidedBy, + RequestedDecision: string(val.RequestedDecision), + Justification: val.Justification, + Approved: val.Approved, + UpdatedAt: val.UpdatedAt, + CreatedAt: val.CreatedAt, + } + } + + return &GetAutoDecisionRequestsOutput{Body: extendedRequests}, nil +} + +type RequestAutoDecisionOutput struct { + Body AutoDecisionRequestDto +} + +func (h *handler) handleRequestAutoDecision(ctx context.Context, input *struct { + Body CreateAutoDecisionRequestDto +}) (*RequestAutoDecisionOutput, error) { + userCtx := ctxutils.GetUserFromCtx(ctx) + + if userCtx == nil { + return nil, huma.Error400BadRequest("Failed to get current user info") + } + + req, err := h.applicationService.RequestAutoDecision(ctx, input.Body, userCtx.UserID, userCtx.Role) + + if err != nil { + return nil, huma.Error500InternalServerError("Unable to request decision") + } + + return &RequestAutoDecisionOutput{Body: AutoDecisionRequestDto{ + ID: req.ID, + ApplicationID: req.ApplicationID, + RequestedDecision: string(req.RequestedDecision), + Justification: req.Justification, + AutoDecisionApproved: *req.Approved, + CreatedAt: req.CreatedAt, + DecidedBy: req.DecidedBy, + }}, nil +} + +type DeleteAutoDecisionRequest struct { + RequestId uuid.UUID `json:"requestId"` +} + +type DeleteAutoDecisionOutput struct { + Status int +} + +func (h *handler) handleDeleteAutoDecision(ctx context.Context, input *struct { + Body DeleteAutoDecisionRequest +}) (*DeleteAutoDecisionOutput, error) { + userCtx := ctxutils.GetUserFromCtx(ctx) + + if userCtx == nil { + return nil, huma.Error400BadRequest("Failed to get current user info") + } + + err := h.applicationService.DeleteAutoDecisionRequest(ctx, input.Body.RequestId, userCtx.UserID) + + if err != nil { + return nil, huma.Error500InternalServerError("Unable to delete auto decision request") + } + + return &DeleteAutoDecisionOutput{Status: http.StatusOK}, nil +} + +type UpdateAutoDecisionOutput struct { + Status int +} + +func (h *handler) handleUpdateAutoDecision(ctx context.Context, input *struct { + Body UpdateAutoDecisionRequestDto +}) (*UpdateAutoDecisionOutput, error) { + userCtx := ctxutils.GetUserFromCtx(ctx) + + if userCtx == nil { + return nil, huma.Error400BadRequest("Failed to get current user info") + } + + err := h.applicationService.UpdateAutoDecisionRequest( + ctx, + input.Body, + userCtx.UserID, + ) + + if err != nil { + return nil, huma.Error500InternalServerError("Unable to update auto decision request") + } + + return &UpdateAutoDecisionOutput{Status: http.StatusOK}, nil +} + +type ResetApplicationReviewsOutput struct { + Status int +} + +func (h *handler) handleResetApplicationReviews(ctx context.Context, input *struct{}) (*ResetApplicationReviewsOutput, error) { + err := h.applicationService.DeleteAllApplicationReviews(ctx) + + if err != nil { + return nil, huma.Error500InternalServerError("Unable to reset application reviews") + } + + return &ResetApplicationReviewsOutput{Status: http.StatusOK}, nil +} + +type WithdrawApplicationOutput struct { + Status int +} + +func (h *handler) handleWithdrawApplication(ctx context.Context, input *struct{}) (*WithdrawApplicationOutput, error) { + userCtx := ctxutils.GetUserFromCtx(ctx) + + if userCtx == nil { + return nil, huma.Error400BadRequest("Failed to get current user info") + } + + if userCtx.Role != sqlc.UserRoleApplicant { + return nil, huma.Error400BadRequest("Not an applicant") + } + + err := h.applicationService.WithdrawApplication(ctx, userCtx.UserID) + + if err != nil { + return nil, huma.Error500InternalServerError("Unable to withdraw attendance") + } + + return &WithdrawApplicationOutput{Status: http.StatusOK}, nil +} + +type ConfirmAttendanceOutput struct { + Status int +} + +func (h *handler) handleConfirmAttendance(ctx context.Context, input *struct{}) (*ConfirmAttendanceOutput, error) { + userCtx := ctxutils.GetUserFromCtx(ctx) + + if userCtx == nil { + return nil, huma.Error400BadRequest("Failed to get current user info") + } + + if userCtx.Role != sqlc.UserRoleApplicant { + return nil, huma.Error400BadRequest("Not an applicant") + } + + err := h.applicationService.ConfirmAttendance(ctx, userCtx.UserID) + + if err != nil { + return nil, huma.Error500InternalServerError("Unable to withdraw attendance") + } + + return &ConfirmAttendanceOutput{Status: http.StatusOK}, nil +} + +// type GetApplicationByUserIdOutput struct { +// Body Application +// } + +// // TODO: Return StaffApplicationResponse instead +// func (h *handler) handleGetApplicationByUserId(ctx context.Context, input *struct { +// UserID string `path:"userId"` +// }) (*GetApplicationByUserIdOutput, error) { +// userID, err := uuid.Parse(input.UserID) + +// if err != nil { +// return nil, huma.Error400BadRequest("Invalid user id") +// } + +// application, err := h.applicationService.GetApplicationByUserId(ctx, userID) + +// if err != nil { +// if errors.Is(err, database.ErrApplicationNotFound) { +// return nil, huma.Error404NotFound("Application not found for user") +// } + +// return nil, huma.Error500InternalServerError("error retrieving application") +// } + +// return &GetApplicationByUserIdOutput{Body: Application{ +// ID: application.ID, +// UserID: application.UserID, +// Status: string(application.Status), +// Application: application.Application, +// CreatedAt: application.CreatedAt, +// SavedAt: application.SavedAt, +// UpdatedAt: application.UpdatedAt, +// SubmittedAt: application.SubmittedAt, +// HackathonID: application.HackathonID, +// }}, nil +// } + +// type GetResumePresignedUrlOutput struct { +// Body string +// } + +// func (h *handler) handleGetResumePresignedUrlByApplicationId(ctx context.Context, input *struct { +// ApplicationId string `path:"applicationId"` +// }) (*GetResumePresignedUrlOutput, error) { +// userCtx := ctxutils.GetUserFromCtx(ctx) + +// if userCtx == nil { +// return nil, huma.Error400BadRequest("Failed to get current user info") +// } + +// applicationId, err := uuid.Parse(input.ApplicationId) + +// if err != nil { +// return nil, huma.Error400BadRequest("Invalid applicationId") +// } + +// if userCtx.Role != sqlc.UserRoleStaff && userCtx.Role != sqlc.UserRoleAdmin && userCtx.UserID != applicationId { +// return nil, huma.Error400BadRequest("You are not allowed to see other ppls resumes :(") +// } + +// request, err := h.applicationService.GetApplicationResumeURL(ctx, applicationId, 600) + +// if err != nil { +// return nil, huma.Error500InternalServerError("Unable to retrieve download url") +// } + +// return &GetResumePresignedUrlOutput{Body: request.URL}, nil +// } + +// type JoinWaitlistOutput struct { +// Status int +// } + +// func (h *handler) handleJoinWaitlist(ctx context.Context, input *struct{}) (*JoinWaitlistOutput, error) { +// userCtx := ctxutils.GetUserFromCtx(ctx) + +// if userCtx == nil { +// return nil, huma.Error400BadRequest("Failed to get current user info") +// } + +// err := h.applicationService.JoinWaitlist(ctx, userCtx.UserID) + +// if err != nil { +// return nil, huma.Error500InternalServerError("Unable to join waitlist") +// } + +// return &JoinWaitlistOutput{Status: http.StatusOK}, nil +// } + +// type WithdrawAcceptanceOutput struct { +// Status int +// } + +// func (h *handler) handleWithdrawAcceptance(ctx context.Context, input *struct{}) (*WithdrawAcceptanceOutput, error) { +// userCtx := ctxutils.GetUserFromCtx(ctx) + +// if userCtx == nil { +// return nil, huma.Error400BadRequest("Failed to get current user info") +// } + +// err := h.applicationService.WithdrawAcceptance(ctx, userCtx.UserID) + +// if err != nil { +// return nil, huma.Error500InternalServerError("Unable to withdraw acceptance") +// } + +// return &WithdrawAcceptanceOutput{Status: http.StatusOK}, nil +// } + +// type TransitionWaitlistedApplicationsOutput struct { +// Status int +// } + +// func (h *handler) handleTransitionWaitlistedApplications(ctx context.Context, input *struct{}) (*TransitionWaitlistedApplicationsOutput, error) { +// // TODO: move these numbers elsewhere to a config file or something +// var acceptanceCount uint32 = 50 +// var acceptanceQuota uint32 = 500 +// err := h.applicationService.TransitionWaitlistedApplications(ctx, acceptanceCount, acceptanceQuota) + +// if err != nil { +// return nil, huma.Error500InternalServerError("Fail to transition waitlisted applications") +// } + +// return &TransitionWaitlistedApplicationsOutput{Status: http.StatusOK}, nil +// } + +// type CalculateAdmissionsRequestOutput struct { +// Status int +// } + +// func (h *handler) handleCalculateAdmissionsRequest(ctx context.Context, input *struct{}) (*CalculateAdmissionsRequestOutput, error) { +// _, err := h.batService.QueueCalculateAdmissionsTask(ctx) + +// if err != nil { +// return nil, huma.Error500InternalServerError("Failed to handle admissions calculation request") +// } + +// return &CalculateAdmissionsRequestOutput{Status: http.StatusOK}, nil +// } + +// type ReleaseDecisionsOutput struct { +// Status int +// } + +// func (h *handler) handleReleaseDecisions(ctx context.Context, input *struct { +// RunId string `path:"runId"` +// }) (*ReleaseDecisionsOutput, error) { +// runId, err := uuid.Parse(input.RunId) + +// if err != nil { +// return nil, huma.Error400BadRequest("Invalid Run Id") +// } + +// err = h.applicationService.ReleaseDecisions(ctx, runId) + +// if err != nil { +// return nil, err +// } + +// return &ReleaseDecisionsOutput{Status: http.StatusOK}, nil + +// } diff --git a/apps/api/internal/domains/application/http.go b/apps/api/internal/domains/application/http.go index 6f510330..23176c5d 100644 --- a/apps/api/internal/domains/application/http.go +++ b/apps/api/internal/domains/application/http.go @@ -1,32 +1,20 @@ package application import ( - "bytes" - "context" - "errors" - "io" "net/http" - "strconv" - "time" "github.com/danielgtaylor/huma/v2" - "github.com/go-playground/validator/v10" - "github.com/google/uuid" "github.com/rs/zerolog" "github.com/swamphacks/core/apps/api/internal/api/cookie" "github.com/swamphacks/core/apps/api/internal/api/middleware" "github.com/swamphacks/core/apps/api/internal/config" - "github.com/swamphacks/core/apps/api/internal/ctxutils" - "github.com/swamphacks/core/apps/api/internal/database" - "github.com/swamphacks/core/apps/api/internal/database/sqlc" - "github.com/swamphacks/core/apps/api/internal/domains/bat" ) func RegisterRoutes(applicationHandler *handler, group huma.API, mw *middleware.Middleware) { huma.Register(group, huma.Operation{ - OperationID: "get-application", + OperationID: "get-my-application", Method: http.MethodGet, - Summary: "Get Application", + Summary: "Get My Application", Description: "Get the application of the current user", Tags: []string{"Application"}, Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma}, @@ -34,33 +22,59 @@ func RegisterRoutes(applicationHandler *handler, group huma.API, mw *middleware. Errors: []int{http.StatusUnauthorized, http.StatusInternalServerError}, Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, DefaultStatus: http.StatusOK, - }, applicationHandler.handleGetApplication) + }, applicationHandler.handleGetMyApplication) + + // huma.Register(group, huma.Operation{ + // OperationID: "get-application-by-user-id", + // Method: http.MethodGet, + // Summary: "Get Application By User ID", + // Description: "Get the application of the specified user", + // Tags: []string{"Application"}, + // Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma, mw.Auth.RequireStaffHuma}, + // Path: "/{userId}", + // Errors: []int{http.StatusUnauthorized, http.StatusInternalServerError}, + // Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, + // DefaultStatus: http.StatusOK, + // }, applicationHandler.handleGetApplicationByUserId) huma.Register(group, huma.Operation{ - OperationID: "save-application", - Method: http.MethodPost, - Summary: "Save Application", - Description: "Save user's progress on the application. File/Upload fields are not saved (eg. resumes).", + OperationID: "get-extended-application-by-id", + Method: http.MethodGet, + Summary: "Get Extended Application By ID", + Description: "Get the full details of an application, including reviews, applicant and other information", Tags: []string{"Application"}, - Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma}, - Path: "/save", + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma, mw.Auth.RequireAdminHuma}, + Path: "/extended/{applicationId}", Errors: []int{http.StatusUnauthorized, http.StatusInternalServerError}, Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, DefaultStatus: http.StatusOK, - }, applicationHandler.handleSaveApplication) + }, applicationHandler.handleGetExtendedApplicationById) huma.Register(group, huma.Operation{ - OperationID: "submit-application", - Method: http.MethodPost, - Summary: "Submit Application", - Description: "Submit the application", + OperationID: "update-application-by-id", + Method: http.MethodPatch, + Summary: "Update Application By ID", + Description: "Update an application", Tags: []string{"Application"}, - Middlewares: huma.Middlewares{mw.Auth.RawHTTPMiddlewareHuma, mw.Auth.RequireAuthHuma}, - Path: "/submit", + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma, mw.Auth.RequireAdminHuma}, + Path: "", Errors: []int{http.StatusUnauthorized, http.StatusInternalServerError}, Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, DefaultStatus: http.StatusOK, - }, applicationHandler.handleSubmitApplication) + }, applicationHandler.handleUpdateApplicationById) + + huma.Register(group, huma.Operation{ + OperationID: "search-applications", + Method: http.MethodGet, + Summary: "Search Applications", + Description: "Search applications for the current hackthaton.", + Tags: []string{"Application"}, + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma, mw.Auth.RequireAdminHuma}, + Path: "/search", + Errors: []int{http.StatusUnauthorized, http.StatusInternalServerError}, + Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, + DefaultStatus: http.StatusOK, + }, applicationHandler.handleSearchApplications) huma.Register(group, huma.Operation{ OperationID: "get-download-resume-url", @@ -73,7 +87,19 @@ func RegisterRoutes(applicationHandler *handler, group huma.API, mw *middleware. Errors: []int{http.StatusUnauthorized, http.StatusInternalServerError}, Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, DefaultStatus: http.StatusOK, - }, applicationHandler.handleGetDownloadResumeURL) + }, applicationHandler.handleGetApplicationResumeURL) + + huma.Register(group, huma.Operation{ + OperationID: "replace-resume", + Method: http.MethodPatch, + Summary: "Replace Resume", + Description: "Replaces the resume of an already-submitted application without modifying any question responses.", + Tags: []string{"Application"}, + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma}, + Path: "/resume", + Errors: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusInternalServerError}, + Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, + }, applicationHandler.handleReplaceResume) huma.Register(group, huma.Operation{ OperationID: "get-application-statistics", @@ -89,659 +115,307 @@ func RegisterRoutes(applicationHandler *handler, group huma.API, mw *middleware. }, applicationHandler.handleGetApplicationStatistics) huma.Register(group, huma.Operation{ - OperationID: "submit-application-review", + OperationID: "save-application", Method: http.MethodPost, - Summary: "Submit Application Review", - Description: "Handles ratings submissions from staff during the application review process", + Summary: "Save Application", + Description: "Save user's progress on the application. File/Upload fields are not saved (eg. resumes).", Tags: []string{"Application"}, - Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma, mw.Auth.RequireStaffHuma}, - Path: "/review/{applicantId}", + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma}, + Path: "/save", Errors: []int{http.StatusUnauthorized, http.StatusInternalServerError}, Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, DefaultStatus: http.StatusOK, - }, applicationHandler.handleSubmitApplicationReview) + }, applicationHandler.handleSaveApplication) huma.Register(group, huma.Operation{ - OperationID: "get-assigned-applications", - Method: http.MethodGet, - Summary: "Get Assigned Applications", - Description: "Returns assigned applications and their review progress for the authenticated reviewer", + OperationID: "submit-application", + Method: http.MethodPost, + Summary: "Submit Application", + Description: "Submit the application", Tags: []string{"Application"}, - Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma, mw.Auth.RequireStaffHuma}, - Path: "/assigned", + Middlewares: huma.Middlewares{mw.Auth.RawHTTPMiddlewareHuma, mw.Auth.RequireAuthHuma}, + Path: "/submit", Errors: []int{http.StatusUnauthorized, http.StatusInternalServerError}, Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, DefaultStatus: http.StatusOK, - }, applicationHandler.handleGetAssignedApplications) + }, applicationHandler.handleSubmitApplication) huma.Register(group, huma.Operation{ - OperationID: "assign-application-reviewers", - Method: http.MethodPost, - Summary: "Assign Application Reviewers", - Description: "Assigns applications to reviewers for the application review process.", + OperationID: "withdraw-application", + Method: http.MethodPatch, + Summary: "Withdraw Application", + Description: "Withdraw application after being accepted to the hackthon. Sets application status from accepted to withdrawn.", Tags: []string{"Application"}, - Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma, mw.Auth.RequireAdminHuma}, - Path: "/review/assign", + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma}, + Path: "/withdraw", Errors: []int{http.StatusUnauthorized, http.StatusInternalServerError}, Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, DefaultStatus: http.StatusOK, - }, applicationHandler.handleAssignApplicationReviewers) + }, applicationHandler.handleWithdrawApplication) huma.Register(group, huma.Operation{ - OperationID: "reset-application-reviews", - Method: http.MethodPost, - Summary: "Reset Application Reviews", - Description: "Resets all application reviews, clearing any existing reviewer assignments.", + OperationID: "confirm-attendance", + Method: http.MethodPatch, + Summary: "Confirm Attendance", + Description: "Confirm attendance after being accepted. Sets event role to attendee from applicant.", Tags: []string{"Application"}, - Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma, mw.Auth.RequireAdminHuma}, - Path: "/review/reset", + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma}, + Path: "/confirm", Errors: []int{http.StatusUnauthorized, http.StatusInternalServerError}, Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, DefaultStatus: http.StatusOK, - }, applicationHandler.handleResetApplicationReviews) + }, applicationHandler.handleConfirmAttendance) + + // REVIEWS huma.Register(group, huma.Operation{ - OperationID: "get-resume", + OperationID: "get-review-assignments", Method: http.MethodGet, - Summary: "Get Resume URL (for review process)", - Description: "Returns a presigned S3 URL with GET permission for a specific user's resume as an object. The client can use this URL to download the object temporarily for application review.", - Tags: []string{"Application"}, + Summary: "Get Review Assignments", + Description: "Get review assignments for the reviewer and review status for each", + Tags: []string{"Application Review"}, Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma, mw.Auth.RequireStaffHuma}, - Path: "/review/{applicantId}/resume", + Path: "/review/assignments", Errors: []int{http.StatusUnauthorized, http.StatusInternalServerError}, Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, DefaultStatus: http.StatusOK, - }, applicationHandler.handleGetResumePresignedUrl) + }, applicationHandler.handleGetReviewAssignments) huma.Register(group, huma.Operation{ - OperationID: "join-waitlist", - Method: http.MethodPatch, - Summary: "Join Waitlist", - Description: "Adds a waitlist join time to application. Sets status to waitlisted", - Tags: []string{"Application"}, - Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma}, - Path: "/join-waitlist", + OperationID: "get-review-by-id", + Method: http.MethodGet, + Summary: "Get Review By Id", + Description: "Get an application review detail including ratings, the application json, and resume", + Tags: []string{"Application Review"}, + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma, mw.Auth.RequireStaffHuma}, + Path: "/review/{reviewId}", Errors: []int{http.StatusUnauthorized, http.StatusInternalServerError}, Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, DefaultStatus: http.StatusOK, - }, applicationHandler.handleJoinWaitlist) + }, applicationHandler.handleGetReviewById) huma.Register(group, huma.Operation{ - OperationID: "withdraw-acceptance", - Method: http.MethodPatch, - Summary: "Withdraw Acceptance", - Description: "Withdraw an acceptance after being accepted to an event. Sets application status from accepted to rejected.", - Tags: []string{"Application"}, - Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma}, - Path: "/withdraw-acceptance", + OperationID: "get-reviewers-and-progress", + Method: http.MethodGet, + Summary: "Get All Reviewers and Progress", + Description: "Get all reviewers and their progress", + Tags: []string{"Application Review"}, + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma, mw.Auth.RequireAdminHuma}, + Path: "/review/progress", Errors: []int{http.StatusUnauthorized, http.StatusInternalServerError}, Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, DefaultStatus: http.StatusOK, - }, applicationHandler.handleWithdrawAcceptance) + }, applicationHandler.handleGetAllReviewersAndProgress) huma.Register(group, huma.Operation{ - OperationID: "withdraw-attendance", - Method: http.MethodPatch, - Summary: "Withdraw Attendance", - Description: "Withdraw attendance after accepting to go to the hackathon. Sets application status from accepted to withdrawn. Sets event role from attendee, back to applicant.", - Tags: []string{"Application"}, - Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma}, - Path: "/withdraw-attendance", + OperationID: "submit-application-review", + Method: http.MethodPost, + Summary: "Submit Application Review", + Description: "Handles ratings submissions from staff during the application review process", + Tags: []string{"Application Review"}, + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma, mw.Auth.RequireStaffHuma}, + Path: "/review", Errors: []int{http.StatusUnauthorized, http.StatusInternalServerError}, Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, DefaultStatus: http.StatusOK, - }, applicationHandler.handleWithdrawAttendance) + }, applicationHandler.handleSubmitApplicationReview) huma.Register(group, huma.Operation{ - OperationID: "accept-application-acceptance", + OperationID: "update-application-review", Method: http.MethodPatch, - Summary: "Accept Application Acceptance", - Description: "Accept an acceptance after being accepted. Sets event role to attendee, from applicant.", - Tags: []string{"Application"}, - Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma}, - Path: "/accept-acceptance", + Summary: "Update Application Review", + Description: "Update application review", + Tags: []string{"Application Review"}, + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma, mw.Auth.RequireAdminHuma}, + Path: "/review", Errors: []int{http.StatusUnauthorized, http.StatusInternalServerError}, Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, DefaultStatus: http.StatusOK, - }, applicationHandler.handleAcceptApplicationAcceptance) + }, applicationHandler.handleUpdateApplicationReview) huma.Register(group, huma.Operation{ - OperationID: "transition-waitlist", - Method: http.MethodPatch, - Summary: "Transition Waitlisted Applications", - Description: "Transitions all accepted users to waitlist, and accepts 50 from the waitlist. Sets application status from accepted to rejected.", - Tags: []string{"Application"}, - Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma}, - Path: "/transition-waitlisted-applications", + OperationID: "update-application-review-status", + Method: http.MethodPost, + Summary: "Update Application Review Status", + Description: "Update the application review status for the current hackathon", + Tags: []string{"Application Review"}, + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma, mw.Auth.RequireAdminHuma}, + Path: "/review/update-status", Errors: []int{http.StatusUnauthorized, http.StatusInternalServerError}, Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, DefaultStatus: http.StatusOK, - }, applicationHandler.handleTransitionWaitlistedApplications) + }, applicationHandler.handleUpdateApplicationReviewStatusForHackathon) huma.Register(group, huma.Operation{ - OperationID: "calculate-admissions-request", + OperationID: "assign-application-reviewers", Method: http.MethodPost, - Summary: "Submit Admissions Calculation Request", - Description: "Queues an admission calculation task to the BAT worker", - Tags: []string{"Application"}, + Summary: "Assign Application Reviewers", + Description: "Assigns applications to reviewers for the application review process.", + Tags: []string{"Application Review"}, Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma, mw.Auth.RequireAdminHuma}, - Path: "/calculate-admissions", + Path: "/review/assign", Errors: []int{http.StatusUnauthorized, http.StatusInternalServerError}, Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, DefaultStatus: http.StatusOK, - }, applicationHandler.handleCalculateAdmissionsRequest) + }, applicationHandler.handleAssignApplicationReviewers) huma.Register(group, huma.Operation{ - OperationID: "release-decisions", + OperationID: "reset-application-reviews", Method: http.MethodPost, - Summary: "Release Decisions", - Description: "Releases decisions that were calculated by the worker from a specific run id", - Tags: []string{"Application"}, + Summary: "Reset Application Reviews", + Description: "Resets all application reviews, clearing any existing reviewer assignments.", + Tags: []string{"Application Review"}, Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma, mw.Auth.RequireAdminHuma}, - Path: "/release-decisions/{runId}", + Path: "/review/reset", Errors: []int{http.StatusUnauthorized, http.StatusInternalServerError}, Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, DefaultStatus: http.StatusOK, - }, applicationHandler.handleReleaseDecisions) + }, applicationHandler.handleResetApplicationReviews) + + huma.Register(group, huma.Operation{ + OperationID: "search-auto-decision-requests", + Method: http.MethodGet, + Summary: "Search Auto Decision Requests", + Description: "Search auto deicision requests", + Tags: []string{"Application Review"}, + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma, mw.Auth.RequireAdminHuma}, + Path: "/review/search-auto-decision-requests", + Errors: []int{http.StatusUnauthorized, http.StatusInternalServerError}, + Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, + DefaultStatus: http.StatusOK, + }, applicationHandler.handleSearchAutoDecisionRequests) + + huma.Register(group, huma.Operation{ + OperationID: "get-auto-decision-requests", + Method: http.MethodGet, + Summary: "Get Auto Decision Requests", + Description: "Get all auto deicision requests created by staff", + Tags: []string{"Application Review"}, + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma, mw.Auth.RequireAdminHuma}, + Path: "/review/all-auto-decision-requests", + Errors: []int{http.StatusUnauthorized, http.StatusInternalServerError}, + Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, + DefaultStatus: http.StatusOK, + }, applicationHandler.handleGetAutoDecisionRequests) + + huma.Register(group, huma.Operation{ + OperationID: "request-auto-decision", + Method: http.MethodPost, + Summary: "Request Auto Decision", + Description: "Create a request to auto accept or auto reject applications.", + Tags: []string{"Application Review"}, + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma, mw.Auth.RequireStaffHuma}, + Path: "/review/auto-decision", + Errors: []int{http.StatusUnauthorized, http.StatusInternalServerError}, + Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, + DefaultStatus: http.StatusOK, + }, applicationHandler.handleRequestAutoDecision) + + huma.Register(group, huma.Operation{ + OperationID: "delete-auto-decision", + Method: http.MethodDelete, + Summary: "Delete Auto Decision", + Description: "Delete an existing auto decision made by current reviewer", + Tags: []string{"Application Review"}, + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma, mw.Auth.RequireStaffHuma}, + Path: "/review/auto-decision", + Errors: []int{http.StatusUnauthorized, http.StatusInternalServerError}, + Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, + DefaultStatus: http.StatusOK, + }, applicationHandler.handleDeleteAutoDecision) + + huma.Register(group, huma.Operation{ + OperationID: "update-auto-decision-request", + Method: http.MethodPatch, + Summary: "Update Auto Decision Request", + Description: "Update an auto decision request", + Tags: []string{"Application Review"}, + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma, mw.Auth.RequireAdminHuma}, + Path: "/review/auto-decision", + Errors: []int{http.StatusUnauthorized, http.StatusInternalServerError}, + Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, + DefaultStatus: http.StatusOK, + }, applicationHandler.handleUpdateAutoDecision) + + // huma.Register(group, huma.Operation{ + // OperationID: "join-waitlist", + // Method: http.MethodPatch, + // Summary: "Join Waitlist", + // Description: "Adds a waitlist join time to application. Sets status to waitlisted", + // Tags: []string{"Application"}, + // Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma}, + // Path: "/join-waitlist", + // Errors: []int{http.StatusUnauthorized, http.StatusInternalServerError}, + // Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, + // DefaultStatus: http.StatusOK, + // }, applicationHandler.handleJoinWaitlist) + + // huma.Register(group, huma.Operation{ + // OperationID: "withdraw-acceptance", + // Method: http.MethodPatch, + // Summary: "Withdraw Acceptance", + // Description: "Withdraw an acceptance after being accepted to an event. Sets application status from accepted to rejected.", + // Tags: []string{"Application"}, + // Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma}, + // Path: "/withdraw-acceptance", + // Errors: []int{http.StatusUnauthorized, http.StatusInternalServerError}, + // Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, + // DefaultStatus: http.StatusOK, + // }, applicationHandler.handleWithdrawAcceptance) + + // huma.Register(group, huma.Operation{ + // OperationID: "transition-waitlist", + // Method: http.MethodPatch, + // Summary: "Transition Waitlisted Applications", + // Description: "Transitions all accepted users to waitlist, and accepts 50 from the waitlist. Sets application status from accepted to rejected.", + // Tags: []string{"Application"}, + // Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma}, + // Path: "/transition-waitlisted-applications", + // Errors: []int{http.StatusUnauthorized, http.StatusInternalServerError}, + // Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, + // DefaultStatus: http.StatusOK, + // }, applicationHandler.handleTransitionWaitlistedApplications) + + // huma.Register(group, huma.Operation{ + // OperationID: "calculate-admissions-request", + // Method: http.MethodPost, + // Summary: "Submit Admissions Calculation Request", + // Description: "Queues an admission calculation task to the BAT worker", + // Tags: []string{"Application"}, + // Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma, mw.Auth.RequireAdminHuma}, + // Path: "/calculate-admissions", + // Errors: []int{http.StatusUnauthorized, http.StatusInternalServerError}, + // Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, + // DefaultStatus: http.StatusOK, + // }, applicationHandler.handleCalculateAdmissionsRequest) + + // huma.Register(group, huma.Operation{ + // OperationID: "release-decisions", + // Method: http.MethodPost, + // Summary: "Release Decisions", + // Description: "Releases decisions that were calculated by the worker from a specific run id", + // Tags: []string{"Application"}, + // Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma, mw.Auth.RequireAdminHuma}, + // Path: "/release-decisions/{runId}", + // Errors: []int{http.StatusUnauthorized, http.StatusInternalServerError}, + // Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, + // DefaultStatus: http.StatusOK, + // }, applicationHandler.handleReleaseDecisions) } type handler struct { applicationService *ApplicationService - batService *bat.BatService config *config.Config logger zerolog.Logger } func NewHandler( - applicationService *ApplicationService, batService *bat.BatService, + applicationService *ApplicationService, config *config.Config, logger zerolog.Logger, ) *handler { return &handler{ applicationService: applicationService, - batService: batService, config: config, logger: logger, } } - -type HackerApplication struct { - UserID uuid.UUID `json:"userId"` - Status sqlc.ApplicationStatus `json:"status"` - Application []byte `json:"application"` - CreatedAt time.Time `json:"createdAt"` - SavedAt time.Time `json:"savedAt"` - UpdatedAt time.Time `json:"updatedAt"` - SubmittedAt *time.Time `json:"submittedAt"` - HackathonID string `json:"hackathonId"` -} - -type GetApplicationOutput struct { - Body HackerApplication -} - -func (h *handler) handleGetApplication(ctx context.Context, input *struct{}) (*GetApplicationOutput, error) { - userCtx := ctxutils.GetUserFromCtx(ctx) - - if userCtx == nil { - return nil, huma.Error400BadRequest("Failed to get current user info") - } - - application, err := h.applicationService.GetApplicationByUserId(ctx, userCtx.UserID) - if err != nil { - if errors.Is(err, database.ErrApplicationNotFound) { - newApplication, err := h.applicationService.CreateApplication(ctx, userCtx.UserID) - - if err != nil || newApplication == nil { - return nil, huma.Error500InternalServerError(err.Error()) - } - - return &GetApplicationOutput{Body: HackerApplication{ - UserID: newApplication.UserID, - Status: newApplication.Status, - Application: newApplication.Application, - CreatedAt: newApplication.CreatedAt, - SavedAt: newApplication.SavedAt, - UpdatedAt: newApplication.UpdatedAt, - SubmittedAt: newApplication.SubmittedAt, - HackathonID: newApplication.HackathonID, - }}, nil - } - - if errors.Is(err, ErrApplicationNotOpened) { - return nil, huma.Error400BadRequest(err.Error()) - } - - return nil, huma.Error500InternalServerError("error retrieving application") - } - - return &GetApplicationOutput{Body: HackerApplication{ - UserID: application.UserID, - Status: application.Status, - Application: application.Application, - CreatedAt: application.CreatedAt, - SavedAt: application.SavedAt, - UpdatedAt: application.UpdatedAt, - SubmittedAt: application.SubmittedAt, - HackathonID: application.HackathonID, - }}, nil -} - -type SaveApplicationOutput struct { - Status int -} - -func (h *handler) handleSaveApplication(ctx context.Context, input *struct { - Body any -}) (*SaveApplicationOutput, error) { - userCtx := ctxutils.GetUserFromCtx(ctx) - - if userCtx == nil { - return nil, huma.Error400BadRequest("Failed to get current user info") - } - - // TODO: validate input.Body, make sure that the data is the application - err := h.applicationService.SaveApplication(ctx, input.Body, userCtx.UserID) - - if err != nil { - return nil, huma.Error500InternalServerError(err.Error()) - } - - return &SaveApplicationOutput{Status: http.StatusOK}, nil -} - -type SubmissionResult struct { - SubmittedAt *time.Time `json:"submittedAt"` -} - -type SubmitApplicationOutput struct { - Body SubmissionResult -} - -func (h *handler) handleSubmitApplication(ctx context.Context, input *struct{}) (*SubmitApplicationOutput, error) { - userCtx := ctxutils.GetUserFromCtx(ctx) - - if userCtx == nil { - return nil, huma.Error400BadRequest("Failed to get current user info") - } - - // TODO: refactor this to using Huma's request API instead of using the raw http package - - r := ctx.Value(middleware.RawRequestKey{}).(*http.Request) - - // Parse multipart form (10 MB max memory) - err := r.ParseMultipartForm(10 << 20) - if err != nil { - return nil, huma.Error400BadRequest("Failed to parse form") - } - - var submission ApplicationSubmissionFields - - // Map form values - submission.FirstName = r.FormValue("firstName") - submission.LastName = r.FormValue("lastName") - - if ageStr := r.FormValue("age"); ageStr != "" { - if age, err := strconv.Atoi(ageStr); err == nil { - submission.Age = age - } - } - - submission.Phone = r.FormValue("phone") - submission.PreferredEmail = r.FormValue("preferredEmail") - submission.UniversityEmail = r.FormValue("universityEmail") - - submission.Country = r.FormValue("country") - submission.Gender = r.FormValue("gender") - submission.GenderOther = r.FormValue("gender-other") - submission.Pronouns = r.FormValue("pronouns") - submission.Race = r.FormValue("race") - submission.RaceOther = r.FormValue("race-other") - submission.Orientation = r.FormValue("orientation") - - submission.Linkedin = r.FormValue("linkedin") - submission.Github = r.FormValue("github") - - if ageCertStr := r.FormValue("ageCertification"); ageCertStr != "" { - submission.AgeCertification = (ageCertStr == "true" || ageCertStr == "1") - } - - submission.School = r.FormValue("school") - submission.Level = r.FormValue("level") - submission.LevelOther = r.FormValue("level-other") - submission.Year = r.FormValue("year") - submission.YearOther = r.FormValue("year-other") - submission.GraduationYear = r.FormValue("graduationYear") - submission.Majors = r.FormValue("majors") - submission.Minors = r.FormValue("minors") - submission.Experience = r.FormValue("experience") - submission.UfHackathonExp = r.FormValue("ufHackathonExp") - submission.ProjectExperience = r.FormValue("projectExperience") - submission.ShirtSize = r.FormValue("shirtSize") - submission.Diet = r.FormValue("diet") - submission.Essay1 = r.FormValue("essay1") - submission.Essay2 = r.FormValue("essay2") - submission.Referral = r.FormValue("referral") - submission.PictureConsent = r.FormValue("pictureConsent") - submission.InPersonAcknowledgement = r.FormValue("inpersonAcknowledgement") - submission.AgreeToConduct = r.FormValue("agreeToConduct") - submission.InfoShareAuthorization = r.FormValue("infoShareAuthorization") - submission.AgreeToMLHEmails = r.FormValue("agreeToMLHEmails") - - resumeFile, _, err := r.FormFile("resume[]") - if err != nil { - return nil, huma.Error400BadRequest("Invalid resume file") - } - - defer resumeFile.Close() - - resumeFileBuffer := bytes.NewBuffer(nil) - - if _, err := io.Copy(resumeFileBuffer, resumeFile); err != nil { - return nil, huma.Error500InternalServerError("Error while parsing resume") - } - - validate := validator.New() - if err := validate.Struct(submission); err != nil { - return nil, huma.Error400BadRequest("Unable to parse application submission") - } - - submittedAt, err := h.applicationService.SubmitApplication(r.Context(), submission, resumeFileBuffer.Bytes(), userCtx.UserID) - - if err != nil { - if errors.Is(err, ErrApplicationNotOpened) { - return nil, huma.Error400BadRequest("Application is not opened") - } - - return nil, huma.Error500InternalServerError("Fail to submit application") - } - - return &SubmitApplicationOutput{ - Body: SubmissionResult{ - SubmittedAt: submittedAt, - }, - }, nil -} - -type GetDownloadResumeOutput struct { - Body string -} - -func (h *handler) handleGetDownloadResumeURL(ctx context.Context, input *struct{}) (*GetDownloadResumeOutput, error) { - userCtx := ctxutils.GetUserFromCtx(ctx) - - if userCtx == nil { - return nil, huma.Error400BadRequest("Failed to get current user info") - } - - request, err := h.applicationService.GetDownloadResumeURL(ctx, userCtx.UserID, 60) - - if err != nil { - return nil, huma.Error500InternalServerError("Unable to get url to download resume") - } - - return &GetDownloadResumeOutput{Body: request.URL}, nil -} - -type GetApplicationStatisticsOutput struct { - Body *ApplicationStatistics -} - -func (h *handler) handleGetApplicationStatistics(ctx context.Context, input *struct{}) (*GetApplicationStatisticsOutput, error) { - stats, err := h.applicationService.GetApplicationStatistics(ctx) - - if err != nil { - return nil, huma.Error500InternalServerError("Unable to get application statistics") - } - - return &GetApplicationStatisticsOutput{Body: stats}, nil -} - -type ReviewRatings struct { - PassionRating int `json:"passionRating" minLength:"1" maxLength:"5" required:"true"` - ExperienceRating int `json:"experienceRating" minLength:"1" maxLength:"5" required:"true"` -} - -type SubmitApplicationReviewOutput struct { - Status int -} - -func (h *handler) handleSubmitApplicationReview(ctx context.Context, input *struct { - ApplicantId string `path:"applicantId"` - Body ReviewRatings -}) (*SubmitApplicationReviewOutput, error) { - applicantId, err := uuid.Parse(input.ApplicantId) - - if err != nil { - return nil, huma.Error400BadRequest("Invalid applicant id") - } - - userCtx := ctxutils.GetUserFromCtx(ctx) - - if userCtx == nil { - return nil, huma.Error400BadRequest("Failed to get current user info") - } - - err = h.applicationService.SaveApplicationReview(ctx, userCtx.UserID, applicantId, input.Body.ExperienceRating, input.Body.PassionRating) - - if err != nil { - return nil, huma.Error500InternalServerError("Unable to save review") - } - - return &SubmitApplicationReviewOutput{Status: http.StatusCreated}, nil -} - -type GetAssignedApplicationsOutput struct { - Body []AssignedApplication -} - -func (h *handler) handleGetAssignedApplications(ctx context.Context, input *struct{}) (*GetAssignedApplicationsOutput, error) { - userCtx := ctxutils.GetUserFromCtx(ctx) - - if userCtx == nil { - return nil, huma.Error400BadRequest("Failed to get current user info") - } - - applications, err := h.applicationService.GetAssignedApplicationsAndProgress(ctx, userCtx.UserID) - - if err != nil { - return nil, huma.Error500InternalServerError("Unable to get assigned applications") - } - - return &GetAssignedApplicationsOutput{Body: applications}, nil -} - -type AssignApplicationReviewersOutput struct { - Status int -} - -func (h *handler) handleAssignApplicationReviewers(ctx context.Context, input *struct { - Body []ReviewerAssignment -}) (*AssignApplicationReviewersOutput, error) { - err := h.applicationService.AssignReviewers(ctx, input.Body) - - if err != nil { - return nil, huma.Error500InternalServerError("Unable to assign reviewers") - } - - return &AssignApplicationReviewersOutput{Status: http.StatusOK}, nil -} - -type ResetApplicationReviewsOutput struct { - Status int -} - -func (h *handler) handleResetApplicationReviews(ctx context.Context, input *struct{}) (*ResetApplicationReviewsOutput, error) { - err := h.applicationService.ResetApplicationReviews(ctx) - - if err != nil { - return nil, huma.Error500InternalServerError("Unable to reset application reviews") - } - - return &ResetApplicationReviewsOutput{Status: http.StatusOK}, nil -} - -type GetResumePresignedUrlOutput struct { - Body string -} - -func (h *handler) handleGetResumePresignedUrl(ctx context.Context, input *struct { - ApplicantId string `path:"applicantId"` -}) (*GetResumePresignedUrlOutput, error) { - userCtx := ctxutils.GetUserFromCtx(ctx) - - if userCtx == nil { - return nil, huma.Error400BadRequest("Failed to get current user info") - } - - applicantId, err := uuid.Parse(input.ApplicantId) - - if err != nil { - return nil, huma.Error400BadRequest("Invalid applicant id") - } - - if userCtx.Role != sqlc.UserRoleStaff && userCtx.Role != sqlc.UserRoleAdmin && userCtx.UserID != applicantId { - return nil, huma.Error400BadRequest("You are not allowed to see other ppls resumes :(") - } - - request, err := h.applicationService.GetDownloadResumeURL(ctx, applicantId, 600) - - if err != nil { - return nil, huma.Error500InternalServerError("Unable to retrieve download url") - } - - return &GetResumePresignedUrlOutput{Body: request.URL}, nil -} - -type JoinWaitlistOutput struct { - Status int -} - -func (h *handler) handleJoinWaitlist(ctx context.Context, input *struct{}) (*JoinWaitlistOutput, error) { - userCtx := ctxutils.GetUserFromCtx(ctx) - - if userCtx == nil { - return nil, huma.Error400BadRequest("Failed to get current user info") - } - - err := h.applicationService.JoinWaitlist(ctx, userCtx.UserID) - - if err != nil { - return nil, huma.Error500InternalServerError("Unable to join waitlist") - } - - return &JoinWaitlistOutput{Status: http.StatusOK}, nil -} - -type WithdrawAcceptanceOutput struct { - Status int -} - -func (h *handler) handleWithdrawAcceptance(ctx context.Context, input *struct{}) (*WithdrawAcceptanceOutput, error) { - userCtx := ctxutils.GetUserFromCtx(ctx) - - if userCtx == nil { - return nil, huma.Error400BadRequest("Failed to get current user info") - } - - err := h.applicationService.WithdrawAcceptance(ctx, userCtx.UserID) - - if err != nil { - return nil, huma.Error500InternalServerError("Unable to withdraw acceptance") - } - - return &WithdrawAcceptanceOutput{Status: http.StatusOK}, nil -} - -type WithdrawAttendanceOutput struct { - Status int -} - -func (h *handler) handleWithdrawAttendance(ctx context.Context, input *struct{}) (*WithdrawAttendanceOutput, error) { - userCtx := ctxutils.GetUserFromCtx(ctx) - - if userCtx == nil { - return nil, huma.Error400BadRequest("Failed to get current user info") - } - - err := h.applicationService.WithdrawAttendance(ctx, userCtx.UserID) - - if err != nil { - return nil, huma.Error500InternalServerError("Unable to withdraw attendance") - } - - return &WithdrawAttendanceOutput{Status: http.StatusOK}, nil -} - -type AcceptApplicationAcceptanceOutput struct { - Status int -} - -func (h *handler) handleAcceptApplicationAcceptance(ctx context.Context, input *struct{}) (*AcceptApplicationAcceptanceOutput, error) { - userCtx := ctxutils.GetUserFromCtx(ctx) - - if userCtx == nil { - return nil, huma.Error400BadRequest("Failed to get current user info") - } - - err := h.applicationService.AcceptApplicationAcceptance(ctx, userCtx.UserID) - - if err != nil { - return nil, huma.Error500InternalServerError("Unable to withdraw attendance") - } - - return &AcceptApplicationAcceptanceOutput{Status: http.StatusOK}, nil -} - -type TransitionWaitlistedApplicationsOutput struct { - Status int -} - -func (h *handler) handleTransitionWaitlistedApplications(ctx context.Context, input *struct{}) (*TransitionWaitlistedApplicationsOutput, error) { - // TODO: move these numbers elsewhere to a config file or something - var acceptanceCount uint32 = 50 - var acceptanceQuota uint32 = 500 - err := h.applicationService.TransitionWaitlistedApplications(ctx, acceptanceCount, acceptanceQuota) - - if err != nil { - return nil, huma.Error500InternalServerError("Fail to transition waitlisted applications") - } - - return &TransitionWaitlistedApplicationsOutput{Status: http.StatusOK}, nil -} - -type CalculateAdmissionsRequestOutput struct { - Status int -} - -func (h *handler) handleCalculateAdmissionsRequest(ctx context.Context, input *struct{}) (*CalculateAdmissionsRequestOutput, error) { - _, err := h.batService.QueueCalculateAdmissionsTask(ctx) - - if err != nil { - return nil, huma.Error500InternalServerError("Failed to handle admissions calculation request") - } - - return &CalculateAdmissionsRequestOutput{Status: http.StatusOK}, nil -} - -type ReleaseDecisionsOutput struct { - Status int -} - -func (h *handler) handleReleaseDecisions(ctx context.Context, input *struct { - RunId string `path:"runId"` -}) (*ReleaseDecisionsOutput, error) { - runId, err := uuid.Parse(input.RunId) - - if err != nil { - return nil, huma.Error400BadRequest("Invalid Run Id") - } - - err = h.applicationService.ReleaseDecisions(ctx, runId) - - if err != nil { - return nil, err - } - - return &ReleaseDecisionsOutput{Status: http.StatusOK}, nil - -} diff --git a/apps/api/internal/domains/application/service.go b/apps/api/internal/domains/application/service.go index 1b24b147..c61774a4 100644 --- a/apps/api/internal/domains/application/service.go +++ b/apps/api/internal/domains/application/service.go @@ -4,6 +4,8 @@ import ( "context" "encoding/json" "errors" + "fmt" + "slices" "time" "github.com/google/uuid" @@ -12,56 +14,41 @@ import ( "github.com/rs/zerolog" "github.com/swamphacks/core/apps/api/internal/config" "github.com/swamphacks/core/apps/api/internal/database" - "github.com/swamphacks/core/apps/api/internal/database/repository" "github.com/swamphacks/core/apps/api/internal/database/sqlc" - "github.com/swamphacks/core/apps/api/internal/domains/bat" "github.com/swamphacks/core/apps/api/internal/domains/email" "github.com/swamphacks/core/apps/api/internal/storage" "golang.org/x/sync/errgroup" ) -var ( - ErrApplicationNotOpened = errors.New("Application not opened") - ErrFailedToCreateApplication = errors.New("Failed to create application") - ErrFailedToGetHackathon = errors.New("Failed to get hackathon information") -) - type ApplicationService struct { - userRepo *repository.UserRepository - applicationRepo *repository.ApplicationRepository - hackathonRepo *repository.HackathonRepository - storage storage.Storage - buckets *config.CoreBuckets - txm *database.TransactionManager - scheduler *asynq.Scheduler - emailService *email.EmailService - batService *bat.BatService - config *config.Config - logger zerolog.Logger + db *database.DB + storage storage.Storage + buckets *config.CoreBuckets + txm *database.TransactionManager + scheduler *asynq.Scheduler + emailService *email.EmailService + config *config.Config + logger zerolog.Logger } func NewService( - applicationRepo *repository.ApplicationRepository, userRepo *repository.UserRepository, - hackathonRepo *repository.HackathonRepository, txm *database.TransactionManager, storage storage.Storage, buckets *config.CoreBuckets, - scheduler *asynq.Scheduler, emailService *email.EmailService, batService *bat.BatService, config *config.Config, logger zerolog.Logger, + db *database.DB, txm *database.TransactionManager, storage storage.Storage, buckets *config.CoreBuckets, + scheduler *asynq.Scheduler, emailService *email.EmailService, config *config.Config, logger zerolog.Logger, ) *ApplicationService { return &ApplicationService{ - applicationRepo: applicationRepo, - userRepo: userRepo, - hackathonRepo: hackathonRepo, - emailService: emailService, // TODO: is there anyway to structure this? I don't know if it's a good idea to depend on another service - batService: batService, - storage: storage, - buckets: buckets, - txm: txm, - scheduler: scheduler, - config: config, - logger: logger.With().Str("service", "ApplicationService").Str("domain", "application").Logger(), + db: db, + emailService: emailService, + storage: storage, + buckets: buckets, + txm: txm, + scheduler: scheduler, + config: config, + logger: logger.With().Str("service", "ApplicationService").Str("domain", "application").Logger(), } } func (s *ApplicationService) CreateApplication(ctx context.Context, userID uuid.UUID) (*sqlc.Application, error) { - hackathon, err := s.hackathonRepo.GetHackathon(ctx) + hackathon, err := s.db.Query.GetHackathon(ctx) if err != nil { s.logger.Err(err).Msg("Create application fail because can't retrieve hackathon") @@ -79,10 +66,9 @@ func (s *ApplicationService) CreateApplication(ctx context.Context, userID uuid. return nil, ErrApplicationNotOpened } - // TODO: don't hardcode the hackathonId - application, err := s.applicationRepo.CreateApplication(ctx, sqlc.CreateApplicationParams{ + application, err := s.db.Query.CreateApplication(ctx, sqlc.CreateApplicationParams{ UserID: userID, - HackathonID: "xii", + HackathonID: hackathon.ID, IsEarly: isEarly, }) @@ -91,14 +77,29 @@ func (s *ApplicationService) CreateApplication(ctx context.Context, userID uuid. return nil, ErrFailedToCreateApplication } - return application, nil + return &application, nil +} + +func (s *ApplicationService) GetApplicationById(ctx context.Context, id uuid.UUID) (*sqlc.Application, error) { + application, err := s.db.Query.GetApplicationById(ctx, id) + + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, database.ErrApplicationNotFound + } else { + s.logger.Err(err).Msg("Failed to get application by id") + return nil, err + } + } + + return &application, nil } func (s *ApplicationService) GetApplicationByUserId(ctx context.Context, userID uuid.UUID) (*sqlc.Application, error) { - application, err := s.applicationRepo.GetApplicationByUserId(ctx, userID) + application, err := s.db.Query.GetApplicationByUserId(ctx, userID) if err != nil { - if errors.Is(err, database.ErrApplicationNotFound) { + if errors.Is(err, pgx.ErrNoRows) { return nil, database.ErrApplicationNotFound } else { s.logger.Err(err).Msg("Failed to get application by user id") @@ -106,53 +107,75 @@ func (s *ApplicationService) GetApplicationByUserId(ctx context.Context, userID } } - return application, nil + return &application, nil } -// TODO: figure out a way to create the submission fields dynamically using the json form files with proper validation. -// these fields are only applicable to swamphacks xi, not other events -type ApplicationSubmissionFields struct { - FirstName string `json:"firstName" validate:"required,max=50"` - LastName string `json:"lastName" validate:"required,max=50"` - Age int `json:"age" validate:"required,min=0,max=99"` - Phone string `json:"phone" validate:"required,len=10"` - PreferredEmail string `json:"preferredEmail" validate:"required,email"` - UniversityEmail string `json:"universityEmail" validate:"required,email"` - Country string `json:"country" validate:"required"` - Gender string `json:"gender"` - GenderOther string `json:"gender-other"` - Pronouns string `json:"pronouns"` - Race string `json:"race"` - RaceOther string `json:"race-other"` - Orientation string `json:"orientation"` - Linkedin string `json:"linkedin" validate:"required,url"` - Github string `json:"github" validate:"required,url"` - AgeCertification bool `json:"ageCertification" validate:"required,boolean"` - School string `json:"school" validate:"required"` - Level string `json:"level" validate:"required"` - LevelOther string `json:"level-other"` - Year string `json:"year" validate:"required"` - YearOther string `json:"year-other"` - GraduationYear string `json:"graduationYear" validate:"required"` - Majors string `json:"majors" validate:"required"` - Minors string `json:"minors"` - Experience string `json:"experience" validate:"required"` - UfHackathonExp string `json:"ufHackathonExp" validate:"required"` - ProjectExperience string `json:"projectExperience" validate:"required"` - ShirtSize string `json:"shirtSize" validate:"required"` - Diet string `json:"diet"` - Essay1 string `json:"essay1" validate:"required"` - Essay2 string `json:"essay2" validate:"required"` - Referral string `json:"referral" validate:"required"` - PictureConsent string `json:"pictureConsent" validate:"required"` - InPersonAcknowledgement string `json:"inpersonAcknowledgement" validate:"required"` - AgreeToConduct string `json:"agreeToConduct" validate:"required"` - InfoShareAuthorization string `json:"infoShareAuthorization" validate:"required"` - AgreeToMLHEmails string `json:"agreeToMLHEmails"` +func (s *ApplicationService) UpdateApplicationById(ctx context.Context, req UpdateApplicationRequestDto) error { + params := sqlc.UpdateApplicationByIdParams{ + ID: req.ApplicationID, + } + + if req.Status != nil { + params.StatusDoUpdate = true + params.Status = *req.Status + } + + return s.db.Query.UpdateApplicationById(ctx, params) +} + +func (s *ApplicationService) GetExtendedApplicationById(ctx context.Context, id uuid.UUID) (*sqlc.GetExtendedApplicationByIdRow, error) { + extendedApplication, err := s.db.Query.GetExtendedApplicationById(ctx, id) + + if err != nil { + s.logger.Err(err). + Str("ApplicationId", id.String()). + Msg("GetExtendedApplicationById fail, unable to get extended application") + return nil, err + } + + return &extendedApplication, nil +} + +func (s *ApplicationService) SearchApplications(ctx context.Context, limit, offset int32, search string) (*int64, []sqlc.SearchApplicationsWithUserInfoRow, error) { + hackathon, err := s.db.Query.GetHackathon(ctx) + + if err != nil { + s.logger.Err(err).Msg("SearchApplications fail because can't retrieve hackathon") + return nil, nil, ErrFailedToGetHackathon + } + + g, ctx := errgroup.WithContext(ctx) + + var applicationsCount int64 + var applications []sqlc.SearchApplicationsWithUserInfoRow + + g.Go(func() error { + var err error + applicationsCount, err = s.db.Query.GetApplicationsCount(ctx, hackathon.ID) + return err + }) + + g.Go(func() error { + var err error + applications, err = s.db.Query.SearchApplicationsWithUserInfo(ctx, sqlc.SearchApplicationsWithUserInfoParams{ + HackathonID: hackathon.ID, + Offset: int32(offset * limit), + Limit: int32(limit), + Search: &search, + }) + return err + }) + + if err := g.Wait(); err != nil { + s.logger.Err(err).Msg("SearchApplications fail") + return nil, nil, err + } + + return &applicationsCount, applications, nil } func (s *ApplicationService) SubmitApplication(ctx context.Context, data ApplicationSubmissionFields, resume []byte, userID uuid.UUID) (*time.Time, error) { - hackathon, err := s.hackathonRepo.GetHackathon(ctx) + hackathon, err := s.db.Query.GetHackathon(ctx) if err != nil { s.logger.Err(err).Msg("Submit application fail because can't retrieve hackathon") @@ -178,9 +201,9 @@ func (s *ApplicationService) SubmitApplication(ctx context.Context, data Applica // Submitting application is an atomic operation err = s.txm.WithTx(ctx, func(tx pgx.Tx) error { - txAppRepo := s.applicationRepo.NewTx(tx) + txDB := s.db.NewTX(tx) - err := txAppRepo.UpdateApplication(ctx, sqlc.UpdateApplicationParams{ + err := txDB.Query.UpdateApplicationByUserId(ctx, sqlc.UpdateApplicationByUserIdParams{ UserID: userID, StatusDoUpdate: true, Status: sqlc.ApplicationStatusSubmitted, @@ -207,7 +230,7 @@ func (s *ApplicationService) SubmitApplication(ctx context.Context, data Applica return err } - err = s.userRepo.UpdateRole(ctx, sqlc.UpdateRoleParams{ + err = s.db.Query.UpdateRole(ctx, sqlc.UpdateRoleParams{ UserID: userID, Role: sqlc.UserRoleApplicant, }) @@ -257,12 +280,12 @@ func (s *ApplicationService) SaveApplication(ctx context.Context, data any, user return errors.New("Failed to parse application data") } - err = s.applicationRepo.UpdateApplication(ctx, sqlc.UpdateApplicationParams{ + err = s.db.Query.UpdateApplicationByUserId(ctx, sqlc.UpdateApplicationByUserIdParams{ + UserID: userID, StatusDoUpdate: true, Status: sqlc.ApplicationStatusStarted, ApplicationDoUpdate: true, Application: dataJSON, - UserID: userID, SavedAtDoUpdate: true, SavedAt: time.Now(), }) @@ -275,15 +298,60 @@ func (s *ApplicationService) SaveApplication(ctx context.Context, data any, user return nil } -func (s *ApplicationService) SubmitApplicationReview(ctx context.Context) (*sqlc.Application, error) { - // application, err := s.appRepo.GetAssignedApplicationByUserAndEventID(ctx, params) +func (s *ApplicationService) GetApplicationResumeURL(ctx context.Context, userID uuid.UUID, lifetimeSecs int64) (*storage.PresignedRequest, error) { + hackathon, err := s.db.Query.GetHackathon(ctx) - // if err != nil { - // s.logger.Err(err).Msg(err.Error()) - // return nil, err - // } + presignableStorage, ok := s.storage.(storage.PresignableStorage) + + if !ok { + err := errors.New("unable to type cast `Storage` to `PresignableStorage`") + s.logger.Err(err).Msg("download resume fail storage setup") + return nil, err + } - return nil, nil + if lifetimeSecs <= 0 { + err := errors.New("invalid number of lifetime seconds") + return nil, err + } + + request, err := presignableStorage.PresignGetObject(ctx, s.buckets.ApplicationResumes, hackathon.ID+"/"+userID.String(), lifetimeSecs) + + if err != nil { + s.logger.Err(err).Msg("fail presign get object") + return nil, err + } + + return request, nil +} + +// ReplaceResume overwrites the resume of an already-submitted application without +// touching any of the question responses. Hackers sometimes submit the wrong resume +// and need to swap it out after the fact. +func (s *ApplicationService) ReplaceResume(ctx context.Context, userID uuid.UUID, resume []byte) error { + hackathon, err := s.db.Query.GetHackathon(ctx) + if err != nil { + s.logger.Err(err).Msg("Replace resume fail because can't retrieve hackathon") + return ErrFailedToGetHackathon + } + + application, err := s.GetApplicationByUserId(ctx, userID) + if err != nil { + return err + } + + // Only allow replacing the resume once the application has actually been submitted. + // Before submission the resume is handled as part of the normal submit flow. + if application.Status == sqlc.ApplicationStatusStarted { + return ErrCannotReplaceResume + } + + contentType := "application/pdf" + if err := s.storage.Store(ctx, s.buckets.ApplicationResumes, hackathon.ID+"/"+userID.String(), resume, &contentType); err != nil { + s.logger.Err(err).Msg("Replace resume fail while storing resume") + return err + } + + return nil } func (s *ApplicationService) GetDownloadResumeURL(ctx context.Context, userID uuid.UUID, lifetimeSecs int64) (*storage.PresignedRequest, error) { @@ -300,7 +368,15 @@ func (s *ApplicationService) GetDownloadResumeURL(ctx context.Context, userID uu return nil, err } - request, err := presignableStorage.PresignGetObject(ctx, s.buckets.ApplicationResumes, userID.String(), lifetimeSecs) + hackathon, err := s.db.Query.GetHackathon(ctx) + if err != nil { + s.logger.Err(err).Msg("download resume fail because can't retrieve hackathon") + return nil, ErrFailedToGetHackathon + } + + // Resumes are stored under hackathonID/userID (see SubmitApplication and ReplaceResume), + // so the presigned download key must match that prefix. + request, err := presignableStorage.PresignGetObject(ctx, s.buckets.ApplicationResumes, hackathon.ID+"/"+userID.String(), lifetimeSecs) if err != nil { s.logger.Err(err).Msg("fail presign get object") @@ -310,58 +386,49 @@ func (s *ApplicationService) GetDownloadResumeURL(ctx context.Context, userID uu return request, nil } -type ApplicationStatistics struct { - GenderStatistics sqlc.GetApplicationGenderSplitRow `json:"genderStats"` - AgeStatistics sqlc.GetApplicationAgeSplitRow `json:"ageStats"` - RaceStatistics []sqlc.GetApplicationRaceSplitRow `json:"raceStats"` - MajorStatistics []sqlc.GetApplicationMajorSplitRow `json:"majorStats"` - SchoolStatistics []sqlc.GetApplicationSchoolSplitRow `json:"schoolStats"` - StatusStatistics sqlc.GetApplicationStatusSplitRow `json:"statusStats"` -} - -func (s *ApplicationService) GetApplicationStatistics(ctx context.Context) (*ApplicationStatistics, error) { +func (s *ApplicationService) GetApplicationStatistics(ctx context.Context) (*ApplicationStatisticsDto, error) { g, ctx := errgroup.WithContext(ctx) - var genderStats sqlc.GetApplicationGenderSplitRow - var ageStats sqlc.GetApplicationAgeSplitRow - var raceStats []sqlc.GetApplicationRaceSplitRow - var majorStats []sqlc.GetApplicationMajorSplitRow - var schoolStats []sqlc.GetApplicationSchoolSplitRow - var statusStats sqlc.GetApplicationStatusSplitRow + var genderStats sqlc.GetSubmittedApplicationGendersRow + var ageStats sqlc.GetSubmittedApplicationAgesRow + var raceStats []sqlc.GetSubmittedApplicationRacesRow + var majorStats []sqlc.GetSubmittedApplicationMajorsRow + var schoolStats []sqlc.GetSubmittedApplicationSchoolsRow + var statusStats sqlc.GetApplicationStatusesRow g.Go(func() error { var err error - genderStats, err = s.applicationRepo.GetSubmittedApplicationGenders(ctx) + genderStats, err = s.db.Query.GetSubmittedApplicationGenders(ctx) return err }) g.Go(func() error { var err error - ageStats, err = s.applicationRepo.GetSubmittedApplicationAges(ctx) + ageStats, err = s.db.Query.GetSubmittedApplicationAges(ctx) return err }) g.Go(func() error { var err error - majorStats, err = s.applicationRepo.GetSubmittedApplicationMajors(ctx) + majorStats, err = s.db.Query.GetSubmittedApplicationMajors(ctx) return err }) g.Go(func() error { var err error - raceStats, err = s.applicationRepo.GetSubmittedApplicationRaces(ctx) + raceStats, err = s.db.Query.GetSubmittedApplicationRaces(ctx) return err }) g.Go(func() error { var err error - schoolStats, err = s.applicationRepo.GetSubmittedApplicationSchools(ctx) + schoolStats, err = s.db.Query.GetSubmittedApplicationSchools(ctx) return err }) g.Go(func() error { var err error - statusStats, err = s.applicationRepo.GetApplicationStatuses(ctx) + statusStats, err = s.db.Query.GetApplicationStatuses(ctx) return err }) @@ -370,7 +437,7 @@ func (s *ApplicationService) GetApplicationStatistics(ctx context.Context) (*App return nil, errors.New("Get application stats error") } - return &ApplicationStatistics{ + return &ApplicationStatisticsDto{ GenderStatistics: genderStats, AgeStatistics: ageStats, RaceStatistics: raceStats, @@ -381,83 +448,246 @@ func (s *ApplicationService) GetApplicationStatistics(ctx context.Context) (*App } -type ReviewerAssignment struct { - ID uuid.UUID `json:"userID"` // User/Reviewer ID - Amount *int `json:"amount"` // Number of applications assigned (nil if autoassign) +// func (s *ApplicationService) JoinWaitlist(ctx context.Context, userID uuid.UUID) error { +// err := s.db.Query.WaitlistApplicationByUserId(ctx, userID) +// if err != nil { +// s.logger.Err(err).Msg("Join waitlist fail") +// return err +// } +// return nil +// } + +func (s *ApplicationService) WithdrawApplication(ctx context.Context, userID uuid.UUID) error { + // Make atomic + err := s.txm.WithTx(ctx, func(tx pgx.Tx) error { + txDB := s.db.NewTX(tx) + + if err := txDB.Query.UpdateApplicationByUserId(ctx, sqlc.UpdateApplicationByUserIdParams{ + UserID: userID, + StatusDoUpdate: true, + Status: sqlc.ApplicationStatusWithdrawn, + }); err != nil { + return err + } + + return txDB.Query.UpdateRole(ctx, + sqlc.UpdateRoleParams{ + UserID: userID, + Role: sqlc.UserRoleApplicant, + }, + ) + }) + if err != nil { + s.logger.Err(err).Str("userID", userID.String()).Msg("WithdrawAttendance fail") + return err + } + return nil } -type ReviewerAllocation struct { - ReviewerID uuid.UUID `json:"reviewerIdd"` - AssignedApplicationIDs []uuid.UUID `json:"assignedApplicationIds"` +func (s *ApplicationService) ConfirmAttendance(ctx context.Context, userID uuid.UUID) error { + // Atomic + err := s.txm.WithTx(ctx, func(tx pgx.Tx) error { + txDB := s.db.NewTX(tx) + + application, err := s.db.Query.GetApplicationByUserId(ctx, userID) + + if err != nil { + s.logger.Err(err).Msg("ConfirmAttendance fail, unable to retrieve user application") + return err + } + + if application.Status != sqlc.ApplicationStatusAccepted { + err = errors.New("User is not accepted to hack") + s.logger.Err(err).Msg(fmt.Sprintf("ConfirmAttendance fail, application is not accepted, status: %s", application.Status)) + return err + } + + if err := txDB.Query.UpdateApplicationByUserId(ctx, sqlc.UpdateApplicationByUserIdParams{ + UserID: userID, + StatusDoUpdate: true, + Status: sqlc.ApplicationStatusConfirmed, + }); err != nil { + return err + } + + return txDB.Query.UpdateRole(ctx, + sqlc.UpdateRoleParams{ + UserID: userID, + Role: sqlc.UserRoleAttendee, + }, + ) + }) + + if err != nil { + s.logger.Err(err).Str("userID", userID.String()).Msg("ConfirmAttendance fail") + return err + } + return nil +} + +// ============================== APPLICATION REVIEW FUNCTIONS ============================== + +func (s *ApplicationService) UpdateApplicationReviewStatusForHackathon(ctx context.Context, started bool) error { + err := s.txm.WithTx(ctx, func(tx pgx.Tx) error { + txDB := s.db.NewTX(tx) + + err := txDB.Query.UpdateHackathon(ctx, sqlc.UpdateHackathonParams{ + ApplicationReviewStartedDoUpdate: true, + ApplicationReviewStarted: started, + }) + + if err != nil { + s.logger.Err(err).Msg("UpdateApplicationReviewStatusForHackathon fail because can't update hackathon") + return err + } + + if started { + err = txDB.Query.MarkSubmittedApplicationsAsUnderReview(ctx) + } else { + err = txDB.Query.ResetApplicationsToSubmitted(ctx) + } + + if err != nil { + s.logger.Err(err).Msg("UpdateApplicationReviewStatusForHackathon fail because can't update applications status") + return err + } + + return nil + }) + + if err != nil { + s.logger.Err(err).Msg("StartApplicationReview fail") + return err + } + + return nil } -func (s *ApplicationService) AssignReviewers(ctx context.Context, reviewers []ReviewerAssignment) error { +func (s *ApplicationService) AssignReviewersToApplications(ctx context.Context, assignments []ReviewerAssignmentRequestDto) error { + // TODO: Must check if applications are closed, if we havent released decisions, and more. + type ReviewerAllocation struct { + ReviewerID uuid.UUID `json:"reviewerIdd"` + AssignedApplicationIDs []uuid.UUID `json:"assignedApplicationIds"` + } + + hackathon, err := s.db.Query.GetHackathon(ctx) + + if err != nil { + s.logger.Err(err).Msg("AssignReviewerToApplications fail because can't retrieve hackathon") + return err + } - //TODO: Must check if applications are closed, if we havent released decisions, and more. + if !hackathon.ApplicationReviewStarted { + return errors.New("Application Review has not started") + } - var fixedReviewers []ReviewerAssignment - var autoReviewers []ReviewerAssignment + var fixedAssignments []ReviewerAssignmentRequestDto + var autoAssignments []ReviewerAssignmentRequestDto var totalFixedAmount int - for _, assignee := range reviewers { - if assignee.Amount != nil { - fixedReviewers = append(fixedReviewers, assignee) - totalFixedAmount += *assignee.Amount + for _, assignment := range assignments { + if assignment.Amount != nil { + // Guard negative/zero amounts + if *assignment.Amount <= 0 { + return errors.New("assignment amount must be positive") + } + fixedAssignments = append(fixedAssignments, assignment) + totalFixedAmount += *assignment.Amount } else { - autoReviewers = append(autoReviewers, assignee) + autoAssignments = append(autoAssignments, assignment) } } - availableApplications, err := s.applicationRepo.ListAvailableApplications(ctx) + availableApplications, err := s.db.Query.ListUnderReviewApplicationIds(ctx, hackathon.ID) if err != nil { return err } totalAvailable := len(availableApplications) if totalAvailable == 0 { + s.logger.Info().Msg("no available applications to assign") return nil } if totalFixedAmount > totalAvailable { - return errors.New("the total number of applications does not match the total number of assigned reviews") + return errors.New("total fixed assignment amount exceeds available applications") } - if totalAvailable > totalFixedAmount && len(autoReviewers) == 0 { - return errors.New("the total number of applications does not match the total number of assigned reviews") + if totalAvailable > totalFixedAmount && len(autoAssignments) == 0 { + return errors.New("not enough assignment slots: some applications would remain unassigned and no auto assignments provided") } - var appIndex int = 0 + // helper to take a slice safely + take := func(start, count int) ([]uuid.UUID, error) { + if count == 0 { + return []uuid.UUID{}, nil + } + if start < 0 || count < 0 || start+count > totalAvailable { + return nil, errors.New("allocation out of bounds") + } + // copy to avoid referencing the backing array + out := make([]uuid.UUID, count) + copy(out, availableApplications[start:start+count]) + return out, nil + } + + var appIndex int var finalAllocations []ReviewerAllocation - // Assign fixed - for _, assignee := range fixedReviewers { - amountToAssign := *assignee.Amount + // Assign fixed amounts + for _, assignment := range fixedAssignments { + amountToAssign := *assignment.Amount + if appIndex+amountToAssign > totalAvailable { + return errors.New("not enough available applications to satisfy fixed assignments") + } + + assignedSlice, err := take(appIndex, amountToAssign) + if err != nil { + return err + } - assignedSlice := availableApplications[appIndex : appIndex+amountToAssign] finalAllocations = append(finalAllocations, ReviewerAllocation{ - ReviewerID: assignee.ID, + ReviewerID: assignment.ID, AssignedApplicationIDs: assignedSlice, }) - appIndex += amountToAssign } + // Auto-assign remaining applications evenly remainingApps := totalAvailable - appIndex - if remainingApps > 0 && len(autoReviewers) > 0 { - baseShare := remainingApps / len(autoReviewers) - remainder := remainingApps % len(autoReviewers) + if remainingApps > 0 && len(autoAssignments) > 0 { + baseShare := remainingApps / len(autoAssignments) + remainder := remainingApps % len(autoAssignments) - for index, assignee := range autoReviewers { + for i, assignment := range autoAssignments { assignCount := baseShare - if index < remainder { + if i < remainder { assignCount++ } - assignedSlice := availableApplications[appIndex : appIndex+assignCount] + if assignCount == 0 { + // nothing to assign to this reviewer + finalAllocations = append(finalAllocations, ReviewerAllocation{ + ReviewerID: assignment.ID, + AssignedApplicationIDs: []uuid.UUID{}, + }) + continue + } + + if appIndex+assignCount > totalAvailable { + return errors.New("auto assignment would exceed available applications") + } + + assignedSlice, err := take(appIndex, assignCount) + if err != nil { + return err + } + finalAllocations = append(finalAllocations, ReviewerAllocation{ - ReviewerID: assignee.ID, + ReviewerID: assignment.ID, AssignedApplicationIDs: assignedSlice, }) + appIndex += assignCount } } @@ -467,45 +697,54 @@ func (s *ApplicationService) AssignReviewers(ctx context.Context, reviewers []Re } return s.txm.WithTx(ctx, func(tx pgx.Tx) error { - txAppRepo := s.applicationRepo.NewTx(tx) - txHackathonRepo := s.hackathonRepo.NewTx(tx) + txDB := s.db.NewTX(tx) + + err = txDB.Query.DeleteAllApplicationReviews(ctx) + + if err != nil { + s.logger.Err(err).Msg("unable to reset all application reviews before assigning") + return err + } + + err = txDB.Query.DeleteAllAutoDecisionRequests(ctx) + + if err != nil { + s.logger.Err(err).Msg("unable to delete all decision requests before assigning") + return err + } for _, allocation := range finalAllocations { - err := txAppRepo.AssignApplicationToReview(ctx, sqlc.AssignApplicationsToReviewerParams{ + if len(allocation.AssignedApplicationIDs) == 0 { + continue + } + + err := txDB.Query.AssignReviewerToApplications(ctx, sqlc.AssignReviewerToApplicationsParams{ ReviewerID: allocation.ReviewerID, ApplicationIds: allocation.AssignedApplicationIDs, }) if err != nil { - s.logger.Err(err).Msg("assign applicattion to review fail while allocating") + s.logger.Err(err).Msg("assign application to reviewer failed while allocating") return err } } - return txHackathonRepo.UpdateHackathon(ctx, sqlc.UpdateHackathonParams{ - ApplicationReviewStartedDoUpdate: true, - ApplicationReviewStarted: true, - }) + return nil }) } -func (s *ApplicationService) ResetApplicationReviews(ctx context.Context) error { +func (s *ApplicationService) DeleteAllApplicationReviews(ctx context.Context) error { return s.txm.WithTx(ctx, func(tx pgx.Tx) error { - txAppRepo := s.applicationRepo.NewTx(tx) - txHackathonRepo := s.hackathonRepo.NewTx(tx) + txDB := s.db.NewTX(tx) - err := txAppRepo.ResetApplicationReviews(ctx) + err := txDB.Query.DeleteAllApplicationReviews(ctx) if err != nil { s.logger.Err(err).Msg(err.Error()) return err } - return txHackathonRepo.UpdateHackathon(ctx, sqlc.UpdateHackathonParams{ - ApplicationReviewStartedDoUpdate: true, - ApplicationReviewStarted: false, - }) + return nil }) - } type ApplicationReviewStatus string @@ -515,233 +754,260 @@ const ( ApplicationReviewStatusCompleted ApplicationReviewStatus = "completed" ) -type AssignedApplication struct { - UserID uuid.UUID `json:"applicantId"` - Status ApplicationReviewStatus `json:"status"` -} +func (s *ApplicationService) GetReviewsForReviewer(ctx context.Context, reviewerId uuid.UUID) ([]sqlc.ListReviewsByReviewerIdRow, error) { + reviews, err := s.db.Query.ListReviewsByReviewerId(ctx, reviewerId) -func (s *ApplicationService) GetAssignedApplicationsAndProgress(ctx context.Context, reviewerId uuid.UUID) ([]AssignedApplication, error) { - applications, err := s.applicationRepo.ListApplicationByReviewer(ctx, reviewerId) if err != nil { - s.logger.Err(err).Msg("get assigned applications and progress fail because list application by reviewer failed") + s.logger.Err(err).Msg("get assigned applications and progress fail because get applications by reviewer failed") return nil, err } - var assignedApps []AssignedApplication - for _, app := range applications { - status := ApplicationReviewStatusInProgress - if app.ExperienceRating != nil && app.PassionRating != nil { - status = ApplicationReviewStatusCompleted - } + return reviews, nil +} - assignedApps = append(assignedApps, AssignedApplication{ - UserID: app.UserID, - Status: status, - }) +func (s *ApplicationService) GetAllReviewersAndProgress(ctx context.Context) ([]sqlc.ListReviewersAndProgressRow, error) { + results, err := s.db.Query.ListReviewersAndProgress(ctx) + + if err != nil { + s.logger.Err(err).Msg("GetAllReviewersAndProgress fail") + return nil, err } - return assignedApps, nil + return results, nil } -func (s *ApplicationService) SaveApplicationReview(ctx context.Context, reviewerId, applicantId uuid.UUID, experienceRating, passionRating int) error { - // Log everything for debug - s.logger.Debug().Str("ReviewerId", reviewerId.String()).Str("ApplicantId", applicantId.String()).Int32("Passion Rating", int32(passionRating)).Int32("Experiene Rating", int32(experienceRating)).Msg("Saving app review.") +func (s *ApplicationService) GetReviewById(ctx context.Context, reviewId uuid.UUID) (*sqlc.GetReviewByIdRow, *storage.PresignedRequest, error) { + review, err := s.db.Query.GetReviewById(ctx, reviewId) - // Get the assigned application - application, err := s.applicationRepo.GetApplicationByUserId(ctx, applicantId) if err != nil { - s.logger.Err(err).Msg("SaveApplicationReview fail, unable to get application for user") - return err + s.logger.Err(err).Msg("GetReviewById fail, unable to get review") + return nil, nil, err } - // Ensure the application is assigned to the reviewer - if application.AssignedReviewerID == nil || *application.AssignedReviewerID != reviewerId { - s.logger. - Warn(). - Str("AssignedReviewID", application.AssignedReviewerID.String()). - Str("ReviewID", reviewerId.String()). - Msg("Cannot review this application. either the assigned review is different or is nil.") - return errors.New("an application has been assigned to a reviewer who is not authorized to review it") + resumeRequest, err := s.GetApplicationResumeURL(ctx, review.UserID, 600) + + if err != nil { + s.logger.Err(err).Msg("GetReviewById fail, unable to get resume") + return nil, nil, err + } + + return &review, resumeRequest, nil +} + +func (s *ApplicationService) SaveApplicationReview(ctx context.Context, req SaveReviewRequestDto, reviewerId uuid.UUID, reviewerRole sqlc.UserRole) error { + // Log everything for debug + s.logger.Debug().Str("ReviewerId", reviewerId.String()). + Str("ApplicantId", req.ApplicationId.String()). + Int32("Passion Rating", int32(req.PassionRating)). + Int32("Experiene Rating", int32(req.ExperienceRating)).Msg("Saving app review.") + + if reviewerRole == sqlc.UserRoleStaff { + reviewerIds, err := s.db.Query.ListApplicationReviewersById(ctx, req.ApplicationId) + if err != nil { + s.logger.Err(err). + Str("ApplicationId", req.ApplicationId.String()). + Msg("SaveApplicationReview fail, unable to get reviewers for application") + return err + } + + if !slices.Contains(reviewerIds, reviewerId) { + s.logger. + Warn(). + Str("ReviewID", reviewerId.String()). + Msg("Cannot review this application. either the assigned review is different") + return errors.New("An application has been assigned to a reviewer who is not authorized to review it") + } } - if err = s.applicationRepo.UpdateApplication(ctx, sqlc.UpdateApplicationParams{ - UserID: applicantId, + newExperienceRating := int32(req.ExperienceRating) + newPassionRating := int32(req.PassionRating) + + if err := s.db.Query.UpdateApplicationReview(ctx, sqlc.UpdateApplicationReviewParams{ + ID: req.ReviewId, + ReviewerID: reviewerId, ExperienceRatingDoUpdate: true, - ExperienceRating: int32(experienceRating), + ExperienceRating: &newExperienceRating, PassionRatingDoUpdate: true, - PassionRating: int32(passionRating), - - //TODO: Make it so I don't have to set this! - StatusDoUpdate: false, - Status: sqlc.ApplicationStatusUnderReview, + PassionRating: &newPassionRating, + NotesDoUpdate: true, + Notes: &req.Notes, + UpdatedByDoUpdate: true, + UpdatedBy: &reviewerId, }); err != nil { - s.logger.Err(err).Msg("Something went wrong Updating the application") + s.logger.Err(err).Msg("Something went wrong updating the application review") } return nil } -func (s *ApplicationService) CheckApplicationReviewsComplete(ctx context.Context) (bool, error) { - nonReviewedApplicantUUIDs, err := s.applicationRepo.GetNonReviewedApplications(ctx) - if err != nil { - return false, errors.New("Failed to check application reviews status") +func (s *ApplicationService) UpdateApplicationReview(ctx context.Context, req UpdateReviewRequestDto, reviewerId uuid.UUID) error { + params := sqlc.UpdateApplicationReviewParams{ + ID: req.ReviewId, + ReviewerID: reviewerId, } - return len(nonReviewedApplicantUUIDs) == 0, nil -} + if req.ExperienceRating != nil { + newExperienceRating := int32(*req.ExperienceRating) + params.ExperienceRatingDoUpdate = true + params.ExperienceRating = &newExperienceRating + } -func (s *ApplicationService) JoinWaitlist(ctx context.Context, userID uuid.UUID) error { - err := s.applicationRepo.JoinWaitlist(ctx, userID) - if err != nil { - s.logger.Err(err).Msg("Join waitlist fail") - return err + if req.PassionRating != nil { + newPassionRating := int32(*req.PassionRating) + params.PassionRatingDoUpdate = true + params.PassionRating = &newPassionRating } - return nil -} -func (s *ApplicationService) WithdrawAcceptance(ctx context.Context, userID uuid.UUID) error { - err := s.applicationRepo.UpdateApplication(ctx, sqlc.UpdateApplicationParams{ - UserID: userID, - StatusDoUpdate: true, - Status: sqlc.ApplicationStatusWithdrawn, - }) - if err != nil { - s.logger.Err(err).Msg("WithdrawAcceptance fail, unable to update application") - return err + if req.Notes != nil { + params.NotesDoUpdate = true + params.Notes = req.Notes } - return nil + + params.UpdatedByDoUpdate = true + params.UpdatedBy = &reviewerId + + return s.db.Query.UpdateApplicationReview(ctx, params) } -func (s *ApplicationService) WithdrawAttendance(ctx context.Context, userID uuid.UUID) error { - // Make atomic - err := s.txm.WithTx(ctx, func(tx pgx.Tx) error { - txAppRepo := s.applicationRepo.NewTx(tx) - txUserRepo := s.userRepo.NewTx(tx) +func (s *ApplicationService) SearchAutoDecisionRequests(ctx context.Context, req SearchAutoDecisionRequestsDto) (*int64, []sqlc.SearchAutoDecisionRequestsRow, error) { + g, ctx := errgroup.WithContext(ctx) - if err := txAppRepo.UpdateApplication(ctx, sqlc.UpdateApplicationParams{ - UserID: userID, - StatusDoUpdate: true, - Status: sqlc.ApplicationStatusWithdrawn, - }); err != nil { - return err - } + var requestsCount int64 + var requests []sqlc.SearchAutoDecisionRequestsRow - return txUserRepo.UpdateRole(ctx, - sqlc.UpdateRoleParams{ - UserID: userID, - Role: sqlc.UserRoleApplicant, - }, - ) + g.Go(func() error { + var err error + requestsCount, err = s.db.Query.GetAutoDecisionRequestsCount(ctx) + return err }) - if err != nil { - s.logger.Err(err).Msg("WithdrawAttendance fail") + + g.Go(func() error { + var err error + var decision sqlc.NullApplicationAutoDecisionType + + if req.Decision == "all" { + decision.Valid = false + } else { + decision.Valid = true + decision.ApplicationAutoDecisionType = sqlc.ApplicationAutoDecisionType(req.Decision) + } + + requests, err = s.db.Query.SearchAutoDecisionRequests(ctx, sqlc.SearchAutoDecisionRequestsParams{ + Offset: int32(req.Offset * req.Limit), + Limit: int32(req.Limit), + Search: &req.Search, + Approved: req.Approved, + Decision: decision, + }) return err + }) + + if err := g.Wait(); err != nil { + s.logger.Err(err).Msg("SearchAutoDecisionRequests fail") + return nil, nil, err } - return nil + + return &requestsCount, requests, nil } -func (s *ApplicationService) AcceptApplicationAcceptance(ctx context.Context, userID uuid.UUID) error { - // is a check for a user being accepted necessary here? or is the frontend enough +func (s *ApplicationService) GetAllAutoDecisionRequests(ctx context.Context) ([]sqlc.ListAutoDecisionRequestsRow, error) { + requests, err := s.db.Query.ListAutoDecisionRequests(ctx) - err := s.userRepo.UpdateRole(ctx, - sqlc.UpdateRoleParams{ - UserID: userID, - Role: sqlc.UserRoleAttendee, - }, - ) if err != nil { - s.logger.Err(err).Msg("AcceptApplicationAcceptance fail, unable to update role") - return err + s.logger.Err(err).Msg("GetAllAutoDecisionRequests fail") + return nil, err } - return nil -} -func (s *ApplicationService) TransitionWaitlistedApplications(ctx context.Context, acceptanceCount uint32, acceptanceQuota uint32) error { - var acceptedUserIds []uuid.UUID + return requests, nil +} - ErrEventAlreadyStarted := errors.New("the event has already started") - ErrFailedToGetContactEmail := errors.New("Failed to get contact email") +func (s *ApplicationService) RequestAutoDecision(ctx context.Context, request CreateAutoDecisionRequestDto, reviewerId uuid.UUID, reviewerRole sqlc.UserRole) (*sqlc.ApplicationAutoDecisionRequest, error) { + hackathon, err := s.db.Query.GetHackathon(ctx) - hackathon, err := s.hackathonRepo.GetHackathon(ctx) if err != nil { - s.logger.Err(err).Msg("TransitionWaitlistedApplications fail, unable to get hackathon") - return err + s.logger.Err(err).Msg("RequestAutoDecision fail because can't retrieve hackathon") + return nil, err } - currentTime := time.Now() - if currentTime.After(hackathon.StartTime) { - s.logger.Err(ErrEventAlreadyStarted).Msg("Could not transition waitlisted applications: the event has already started.") - return ErrEventAlreadyStarted + + if !hackathon.ApplicationReviewStarted { + return nil, errors.New("Application Review has not started") } - err = s.txm.WithTx(ctx, func(tx pgx.Tx) error { - txAppRepo := s.applicationRepo.NewTx(tx) + params := sqlc.RequestAutoDecisionParams{ + ApplicationID: request.ApplicationID, + ReviewerID: reviewerId, + RequestedDecision: (sqlc.ApplicationAutoDecisionType)(request.RequestedDecision), + Justification: request.Justification, + } - err := txAppRepo.TransitionAcceptedApplicationsToWaitlist(ctx) - if err != nil { - s.logger.Err(err).Msg(err.Error()) - return err - } + if reviewerRole == sqlc.UserRoleAdmin { + approved := true + params.Approved = &approved + params.DecidedBy = &reviewerId + } - attendeeCount, err := s.applicationRepo.GetAttendeeCount(ctx) - if err != nil { - s.logger.Err(err).Msg("Failed to get total accepted application amount.") - } - if (acceptanceQuota - attendeeCount) <= acceptanceCount { - s.logger.Info().Msgf("Acceptance quota is close, shutting down waitlist transition scheduler. Remaining acceptances: %v - %v <= %v", acceptanceQuota, attendeeCount, acceptanceCount) - if s.scheduler != nil { - // The API also uses this file, and this function can be run from an endpoint so we have to check that the scheduler exists. - // Technically the task should be removed from the scheduler via an scheduler ENTRY_ID. However the scheduler is only running for this task. - s.scheduler.Shutdown() - } - acceptanceCount = acceptanceQuota - attendeeCount - } + req, err := s.db.Query.RequestAutoDecision(ctx, params) - s.logger.Info().Msgf("Acceptance count: %v", acceptanceCount) - acceptedUserIds, err = txAppRepo.TransitionWaitlistedApplicationsToAccepted(ctx, int32(acceptanceCount)) - if err != nil { - s.logger.Err(err).Msg(err.Error()) - return err - } + if err != nil { + s.logger.Err(err).Msg("RequestAutoDecision fail") + return nil, err + } - s.logger.Debug().Msgf("Statuses transitioned: %s", acceptedUserIds) - return nil + return &req, nil +} + +func (s *ApplicationService) DeleteAutoDecisionRequest(ctx context.Context, requestId, reviewerId uuid.UUID) error { + err := s.db.Query.DeleteAutoDecisionRequest(ctx, sqlc.DeleteAutoDecisionRequestParams{ + ID: requestId, + ReviewerID: reviewerId, }) if err != nil { - s.logger.Err(err).Msg(err.Error()) + s.logger.Err(err).Msg("DeleteAutoDecisionRequest fail") return err } - for _, userID := range acceptedUserIds { - userContactInfo, err := s.userRepo.GetUserEmailInfoById(ctx, userID) - if err != nil { - s.logger.Err(err).Msg(err.Error()) - return err - } + return nil +} - contactEmail, ok := userContactInfo.ContactEmail.(string) - if !ok { - return ErrFailedToGetContactEmail - } +func (s *ApplicationService) UpdateAutoDecisionRequest(ctx context.Context, req UpdateAutoDecisionRequestDto, approverId uuid.UUID) error { + err := s.db.Query.UpdateAutoDecisionRequest(ctx, sqlc.UpdateAutoDecisionRequestParams{ + ID: req.RequestID, + ApprovedDoUpdate: true, + Approved: &req.Approved, + ApprovedByDoUpdate: true, + DecidedBy: &approverId, + }) - err = s.emailService.QueueWaitlistAcceptanceEmail(contactEmail, userContactInfo.Name) - if err != nil { - s.logger.Err(err).Msg(err.Error()) - return err - } + if err != nil { + s.logger.Err(err).Msg("UpdateAutoDecisionRequest fail") + return err } return nil } -// Checks if application is opened -// -// Returns nil if yes, otherwise returns error +func (s *ApplicationService) CheckApplicationReviewsComplete(ctx context.Context) (bool, error) { + hackathon, err := s.db.Query.GetHackathon(ctx) + + if err != nil { + s.logger.Err(err).Msg("CheckApplicationReviewsComplete fail because can't retrieve hackathon") + return false, err + } + + underReviewApplicationIds, err := s.db.Query.ListUnderReviewApplicationIds(ctx, hackathon.ID) + if err != nil { + return false, errors.New("Failed to check application reviews status") + } + + return len(underReviewApplicationIds) == 0, nil +} + func (s *ApplicationService) isApplicationOpen(ctx context.Context) error { - hackathon, err := s.hackathonRepo.GetHackathon(ctx) + hackathon, err := s.db.Query.GetHackathon(ctx) if err != nil { - s.logger.Err(err).Msg("Submit application fail because can't retrieve hackathon") + s.logger.Err(err).Msg("IsApplicationsOpen fail because can't retrieve hackathon") return err } @@ -759,40 +1025,118 @@ func (s *ApplicationService) isApplicationOpen(ctx context.Context) error { return nil } -func (s *ApplicationService) ReleaseDecisions(ctx context.Context, batRunId uuid.UUID) error { - batRun, err := s.batService.GetRunById(ctx, batRunId) - - if batRun.Status != sqlc.BatRunStatusCompleted { - return errors.New("This run status is not valid for this action.") - } - - if len(batRun.AcceptedApplicants) == 0 { - return errors.New("No applicants marked as accepted.") - } - - err = s.txm.WithTx(ctx, func(tx pgx.Tx) error { - txAppRepo := s.applicationRepo.NewTx(tx) - - err := txAppRepo.UpdateApplicationsStatuses(ctx, sqlc.UpdateApplicationStatusParams{ - Status: sqlc.ApplicationStatusAccepted, - UserIds: batRun.AcceptedApplicants, - }) - if err != nil { - return err - } - return txAppRepo.UpdateApplicationsStatuses(ctx, sqlc.UpdateApplicationStatusParams{ - Status: sqlc.ApplicationStatusRejected, - UserIds: batRun.RejectedApplicants, - }) - }) - if err != nil { - return err - } - - err = s.emailService.SendDecisionEmails(ctx, batRun) - if err != nil { - return errors.New("Failed to send decision emails") - } - - return nil -} +// func (s *ApplicationService) TransitionWaitlistedApplications(ctx context.Context, acceptanceCount uint32, acceptanceQuota uint32) error { +// var acceptedUserIds []uuid.UUID + +// ErrEventAlreadyStarted := errors.New("the event has already started") +// ErrFailedToGetContactEmail := errors.New("Failed to get contact email") + +// hackathon, err := s.db.Query.GetHackathon(ctx) +// if err != nil { +// s.logger.Err(err).Msg("TransitionWaitlistedApplications fail, unable to get hackathon") +// return err +// } +// currentTime := time.Now() +// if currentTime.After(hackathon.StartTime) { +// s.logger.Err(ErrEventAlreadyStarted).Msg("Could not transition waitlisted applications: the event has already started.") +// return ErrEventAlreadyStarted +// } + +// err = s.txm.WithTx(ctx, func(tx pgx.Tx) error { +// txDB := s.db.NewTX(tx) + +// err := txDB.Query.TransitionAcceptedApplicationsToWaitlist(ctx) +// if err != nil { +// s.logger.Err(err).Msg(err.Error()) +// return err +// } + +// attendeeCount, err := s.db.Query.GetAttendeeCount(ctx) +// if err != nil { +// s.logger.Err(err).Msg("Failed to get total accepted application amount.") +// } +// if (acceptanceQuota - attendeeCount) <= acceptanceCount { +// s.logger.Info().Msgf("Acceptance quota is close, shutting down waitlist transition scheduler. Remaining acceptances: %v - %v <= %v", acceptanceQuota, attendeeCount, acceptanceCount) +// if s.scheduler != nil { +// // The API also uses this file, and this function can be run from an endpoint so we have to check that the scheduler exists. +// // Technically the task should be removed from the scheduler via an scheduler ENTRY_ID. However the scheduler is only running for this task. +// s.scheduler.Shutdown() +// } +// acceptanceCount = acceptanceQuota - attendeeCount +// } + +// s.logger.Info().Msgf("Acceptance count: %v", acceptanceCount) +// acceptedUserIds, err = txDB.Query.TransitionWaitlistedApplicationsToAccepted(ctx, int32(acceptanceCount)) +// if err != nil { +// s.logger.Err(err).Msg(err.Error()) +// return err +// } + +// s.logger.Debug().Msgf("Statuses transitioned: %s", acceptedUserIds) +// return nil +// }) + +// if err != nil { +// s.logger.Err(err).Msg(err.Error()) +// return err +// } + +// for _, userID := range acceptedUserIds { +// userContactInfo, err := s.db.Query.GetUserEmailInfoById(ctx, userID) +// if err != nil { +// s.logger.Err(err).Msg(err.Error()) +// return err +// } + +// contactEmail, ok := userContactInfo.ContactEmail.(string) +// if !ok { +// return ErrFailedToGetContactEmail +// } + +// err = s.emailService.QueueWaitlistAcceptanceEmail(contactEmail, userContactInfo.Name) +// if err != nil { +// s.logger.Err(err).Msg(err.Error()) +// return err +// } +// } + +// return nil +// } + +// func (s *ApplicationService) ReleaseDecisions(ctx context.Context, batRunId uuid.UUID) error { +// batRun, err := s.batService.GetRunById(ctx, batRunId) + +// if batRun.Status != sqlc.BatRunStatusCompleted { +// return errors.New("This run status is not valid for this action.") +// } + +// if len(batRun.AcceptedApplicants) == 0 { +// return errors.New("No applicants marked as accepted.") +// } + +// err = s.txm.WithTx(ctx, func(tx pgx.Tx) error { +// txDB := s.db.NewTX(tx) + +// err := txDB.Query.UpdateApplicationStatusForMultipleUserIds(ctx, sqlc.UpdateApplicationStatusForMultipleUserIdsParams{ +// Status: sqlc.ApplicationStatusAccepted, +// UserIds: batRun.AcceptedApplicants, +// }) +// if err != nil { +// return err +// } +// return txDB.Query.UpdateApplicationStatusForMultipleUserIds(ctx, sqlc.UpdateApplicationStatusForMultipleUserIdsParams{ +// Status: sqlc.ApplicationStatusRejected, +// UserIds: batRun.RejectedApplicants, +// }) +// }) +// if err != nil { +// return err +// } + +// err = s.emailService.SendDecisionEmails(ctx, batRun) +// if err != nil { +// return errors.New("Failed to send decision emails") +// } + +// return nil +// } diff --git a/apps/api/internal/domains/application/types.go b/apps/api/internal/domains/application/types.go new file mode 100644 index 00000000..634e0925 --- /dev/null +++ b/apps/api/internal/domains/application/types.go @@ -0,0 +1,229 @@ +package application + +import ( + "errors" + "time" + + "github.com/google/uuid" + "github.com/swamphacks/core/apps/api/internal/database/sqlc" +) + +var ( + ErrApplicationNotOpened = errors.New("Application not opened") + ErrFailedToCreateApplication = errors.New("Failed to create application") + ErrFailedToGetHackathon = errors.New("Failed to get hackathon information") + ErrCannotReplaceResume = errors.New("cannot replace resume before the application has been submitted") +) + +type AppUser struct { + ID uuid.UUID `json:"id"` + UserName string `json:"name"` + Image *string `json:"image"` + Email *string `json:"email"` +} + +type MyApplicationResponseDto struct { + ID uuid.UUID `json:"id"` + Status string `json:"status"` + Application []byte `json:"application"` + CreatedAt time.Time `json:"createdAt"` + SubmittedAt *time.Time `json:"submittedAt" required:"false"` + UserID uuid.UUID `json:"userId"` + UpdatedAt time.Time `json:"updatedAt"` + SavedAt time.Time `json:"savedAt"` + HackathonID string `json:"hackathonId"` +} + +type ExtendedApplicationResponseDto struct { + ID uuid.UUID `json:"id"` + Status string `json:"status"` + Application []byte `json:"application"` + CreatedAt time.Time `json:"createdAt"` + SubmittedAt *time.Time `json:"submittedAt" required:"false"` + UpdatedAt time.Time `json:"updatedAt"` + User AppUser `json:"user"` + IsEarly bool `json:"isEarly"` + Review *ReviewDto `json:"review" required:"false"` + AutoDecisionRequest *AutoDecisionRequestDto `json:"autoDecisionRequest" required:"false"` + ResumeURL string `json:"resumeUrl"` +} + +type SearchApplicationsResponseDto struct { + Applications []ApplicationWithUserInfoDto `json:"applications"` + Count int64 `json:"count"` +} + +type SubmitApplicationResponseDto struct { + SubmittedAt *time.Time `json:"submittedAt"` +} + +type UpdateApplicationRequestDto struct { + ApplicationID uuid.UUID `json:"applicationId"` + Status *sqlc.ApplicationStatus `json:"status"` +} + +type ApplicationWithUserInfoDto struct { + ID uuid.UUID `json:"id"` + Status string `json:"status"` + Application []byte `json:"application"` + CreatedAt time.Time `json:"createdAt"` + SubmittedAt *time.Time `json:"submittedAt" required:"false"` + UpdatedAt time.Time `json:"updatedAt"` + User AppUser `json:"user"` + IsEarly bool `json:"isEarly"` +} + +type ApplicationReviewResponseDto struct { + ID uuid.UUID `json:"id"` + ExperienceRating *int32 `json:"experienceRating"` + PassionRating *int32 `json:"passionRating"` + Notes *string `json:"notes"` + ReviewUpdatedAt time.Time `json:"reviewUpdatedAt"` + ReviewUpdatedBy *uuid.UUID `json:"reviewUpdatedBy"` + Application []byte `json:"application"` + ResumeURL string `json:"resumeUrl"` + AutoDecisionRequest *AutoDecisionRequestDto `json:"autoDecisionRequest" required:"false"` +} + +type ReviewAssignmentDto struct { + ReviewID uuid.UUID `json:"reviewId"` + ApplicationID uuid.UUID `json:"applicationId"` + UserID uuid.UUID `json:"userId"` + Status ApplicationReviewStatus `json:"status"` +} + +type ReviewDto struct { + ID uuid.UUID `json:"id"` + ExperienceRating *int32 `json:"experienceRating"` + PassionRating *int32 `json:"passionRating"` + Notes *string `json:"notes"` + ReviewUpdatedAt time.Time `json:"reviewUpdatedAt"` + ReviewUpdatedBy *uuid.UUID `json:"reviewUpdatedBy"` + Reviewer AppUser `json:"reviewer"` +} + +type ReviewerAssignmentRequestDto struct { + ID uuid.UUID `json:"userId"` // User/Reviewer ID + Amount *int `json:"amount"` // Number of applications assigned (nil if autoassign) +} + +type ReviewerProgressResponseDto struct { + ID *uuid.UUID `json:"id"` + Name *string `json:"name"` + Image *string `json:"image"` + TotalAssigned int64 `json:"totalAssigned"` + CompletedCount int64 `json:"completedCount"` +} + +type SaveReviewRequestDto struct { + ReviewId uuid.UUID `json:"id"` + ApplicationId uuid.UUID `json:"applicationId"` + ExperienceRating int `json:"experienceRating"` + PassionRating int `json:"passionRating"` + Notes string `json:"notes"` +} + +type UpdateReviewRequestDto struct { + ReviewId uuid.UUID `json:"id"` + PassionRating *int `json:"passionRating"` + ExperienceRating *int `json:"experienceRating"` + Notes *string `json:"notes"` +} + +type CreateAutoDecisionRequestDto struct { + ApplicationID uuid.UUID `json:"applicationId"` + RequestedDecision string `json:"decision"` + Justification *string `json:"justification"` +} + +type UpdateAutoDecisionRequestDto struct { + RequestID uuid.UUID `json:"requestId"` + Approved bool `json:"approved"` +} + +type AutoDecisionRequestDto struct { + ID uuid.UUID `json:"id"` + ApplicationID uuid.UUID `json:"applicationId"` + Justification *string `json:"justification" required:"false"` + CreatedAt time.Time `json:"createdAt"` + RequestedDecision string `json:"decision"` + AutoDecisionApproved bool `json:"approved"` + DecidedBy *uuid.UUID `json:"decidedBy" required:"false"` +} + +type ExtendedAutoDecisionRequestDto struct { + ID uuid.UUID `json:"id"` + ApplicationID uuid.UUID `json:"applicationId"` + Justification *string `json:"justification" required:"false"` + CreatedAt time.Time `json:"createdAt"` + RequestedDecision string `json:"requestedDecision"` + Approved *bool `json:"approved" required:"false"` + UpdatedAt time.Time `json:"updatedAt"` + Reviewer AppUser `json:"reviewer"` + DecidedBy *AppUser `json:"decidedBy" required:"false"` + User AppUser `json:"user"` +} + +type SearchAutoDecisionRequestsDto struct { + Search string `query:"search"` + Approved string `query:"approved"` + Decision string `query:"decision"` + Offset int32 `query:"offset"` + Limit int32 `query:"limit"` +} + +type SearchAutoDecisionRequestsResponseDto struct { + Requests []ExtendedAutoDecisionRequestDto `json:"autoDecisionRequests" nullable:"false"` + Count int64 `json:"count"` +} + +// TODO: figure out a way to create the submission fields dynamically using the json form files with proper validation. +// these fields are only applicable to swamphacks xi, not other events +type ApplicationSubmissionFields struct { + FirstName string `json:"firstName" validate:"required,max=50"` + LastName string `json:"lastName" validate:"required,max=50"` + Age int `json:"age" validate:"required,min=0,max=99"` + Phone string `json:"phone" validate:"required,len=10"` + PreferredEmail string `json:"preferredEmail" validate:"required,email"` + UniversityEmail string `json:"universityEmail" validate:"required,email"` + Country string `json:"country" validate:"required"` + Gender string `json:"gender"` + GenderOther string `json:"gender-other"` + Pronouns string `json:"pronouns"` + Race string `json:"race"` + RaceOther string `json:"race-other"` + Orientation string `json:"orientation"` + Linkedin string `json:"linkedin" validate:"required,url"` + Github string `json:"github" validate:"required,url"` + AgeCertification bool `json:"ageCertification" validate:"required,boolean"` + School string `json:"school" validate:"required"` + Level string `json:"level" validate:"required"` + LevelOther string `json:"level-other"` + Year string `json:"year" validate:"required"` + YearOther string `json:"year-other"` + GraduationYear string `json:"graduationYear" validate:"required"` + Majors string `json:"majors" validate:"required"` + Minors string `json:"minors"` + Experience string `json:"experience" validate:"required"` + UfHackathonExp string `json:"ufHackathonExp" validate:"required"` + ProjectExperience string `json:"projectExperience" validate:"required"` + ShirtSize string `json:"shirtSize" validate:"required"` + Diet string `json:"diet"` + Essay1 string `json:"essay1" validate:"required"` + Essay2 string `json:"essay2" validate:"required"` + Referral string `json:"referral" validate:"required"` + PictureConsent string `json:"pictureConsent" validate:"required"` + InPersonAcknowledgement string `json:"inpersonAcknowledgement" validate:"required"` + AgreeToConduct string `json:"agreeToConduct" validate:"required"` + InfoShareAuthorization string `json:"infoShareAuthorization" validate:"required"` + AgreeToMLHEmails string `json:"agreeToMLHEmails"` +} + +type ApplicationStatisticsDto struct { + GenderStatistics sqlc.GetSubmittedApplicationGendersRow `json:"genderStats"` + AgeStatistics sqlc.GetSubmittedApplicationAgesRow `json:"ageStats"` + RaceStatistics []sqlc.GetSubmittedApplicationRacesRow `json:"raceStats"` + MajorStatistics []sqlc.GetSubmittedApplicationMajorsRow `json:"majorStats"` + SchoolStatistics []sqlc.GetSubmittedApplicationSchoolsRow `json:"schoolStats"` + StatusStatistics sqlc.GetApplicationStatusesRow `json:"statusStats"` +} diff --git a/apps/api/internal/domains/bat/http.go b/apps/api/internal/domains/bat/http.go index aedd5394..f3088d59 100644 --- a/apps/api/internal/domains/bat/http.go +++ b/apps/api/internal/domains/bat/http.go @@ -1,134 +1,134 @@ package bat -import ( - "context" - "net/http" - - "github.com/danielgtaylor/huma/v2" - "github.com/google/uuid" - "github.com/rs/zerolog" - "github.com/swamphacks/core/apps/api/internal/api/middleware" - "github.com/swamphacks/core/apps/api/internal/database/sqlc" -) - -func RegisterRoutes(batHandler *handler, group huma.API, mw *middleware.Middleware) { - huma.Register(group, huma.Operation{ - OperationID: "get-bat-runs", - Method: http.MethodGet, - Summary: "Get Bat Runs", - Description: "Returns all bat runs", - Tags: []string{"Bat"}, - Path: "/runs", - Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma, mw.Auth.RequireAdminHuma}, - Errors: []int{http.StatusInternalServerError, http.StatusUnauthorized}, - }, batHandler.handleGetRuns) - - huma.Register(group, huma.Operation{ - OperationID: "delete-bat-run", - Method: http.MethodDelete, - Summary: "Delete Bat Run", - Description: "Delete a bat run by id", - Tags: []string{"Bat"}, - Path: "/runs/{runId}", - Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma, mw.Auth.RequireAdminHuma}, - Errors: []int{http.StatusInternalServerError, http.StatusBadRequest, http.StatusUnauthorized}, - }, batHandler.handleDeleteRun) - - huma.Register(group, huma.Operation{ - OperationID: "queue-schedule-waitlist-transition-task", - Method: http.MethodPost, - Summary: "Queue Waitlist Transition Task", - Description: "Queues an asynq task that transitions waitlisted applications, running every 3 days.", - Tags: []string{"Bat"}, - Path: "/begin-waitlist-transition", - Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma, mw.Auth.RequireAdminHuma}, - Errors: []int{http.StatusInternalServerError, http.StatusUnauthorized}, - }, batHandler.handleQueueScheduleWaitlistTransitionTask) - - huma.Register(group, huma.Operation{ - OperationID: "queue-shutdown-waitlist-scheduler-task", - Method: http.MethodPost, - Summary: "Queue Shutdown Waitlist Scheduler", - Description: "Shutsdown the scheduler used for the waitlist transition task. Error returned through logs if a scheduler is not active.", - Tags: []string{"Bat"}, - Path: "/shutdown-waitlist-scheduler", - Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma, mw.Auth.RequireAdminHuma}, - Errors: []int{http.StatusInternalServerError, http.StatusUnauthorized}, - }, batHandler.handleQueueShutdownWaitlistSchedulerTask) -} - -type handler struct { - BatService *BatService - logger zerolog.Logger -} - -func NewHandler(BatService *BatService, logger zerolog.Logger) *handler { - return &handler{ - BatService: BatService, - logger: logger.With().Str("handler", "BatHandler").Str("domain", "bat").Logger(), - } -} - -type GetRunsOutput struct { - Body *[]sqlc.BatRun -} - -func (h *handler) handleGetRuns(ctx context.Context, input *struct{}) (*GetRunsOutput, error) { - runs, err := h.BatService.GetRuns(ctx) - - if err != nil { - return nil, huma.Error500InternalServerError("Failed to get bat runs") - } - - return &GetRunsOutput{Body: runs}, nil -} - -type DeleteRunOutput struct { - Status int -} - -func (h *handler) handleDeleteRun(ctx context.Context, input *struct { - RunId string `path:"runId"` -}) (*DeleteRunOutput, error) { - runId, err := uuid.Parse(input.RunId) - - if err != nil { - return nil, huma.Error400BadRequest("Invalid run id") - } - - err = h.BatService.DeleteRunById(ctx, runId) - - if err != nil { - return nil, huma.Error500InternalServerError("Failed to delete run by id") - } - - return &DeleteRunOutput{Status: http.StatusOK}, nil -} - -type QueueScheduleWaitlistTransitionTaskOutput struct { - Status int -} - -func (h *handler) handleQueueScheduleWaitlistTransitionTask(ctx context.Context, input *struct{}) (*QueueScheduleWaitlistTransitionTaskOutput, error) { - err := h.BatService.QueueScheduleWaitlistTransitionTask(ctx) - - if err != nil { - return nil, huma.Error500InternalServerError("Failed to queue schedule waitlist transition task") - } - - return &QueueScheduleWaitlistTransitionTaskOutput{Status: http.StatusOK}, nil -} - -type QueueShutdownWaitlistSchedulerTaskOutput struct { - Status int -} - -func (h *handler) handleQueueShutdownWaitlistSchedulerTask(ctx context.Context, input *struct{}) (*QueueShutdownWaitlistSchedulerTaskOutput, error) { - err := h.BatService.QueueShutdownWaitlistScheduler() - - if err != nil { - return nil, huma.Error500InternalServerError("Failed to queue shutdown waitlist scheduler task") - } - - return &QueueShutdownWaitlistSchedulerTaskOutput{Status: http.StatusOK}, nil -} +// import ( +// "context" +// "net/http" + +// "github.com/danielgtaylor/huma/v2" +// "github.com/google/uuid" +// "github.com/rs/zerolog" +// "github.com/swamphacks/core/apps/api/internal/api/middleware" +// "github.com/swamphacks/core/apps/api/internal/database/sqlc" +// ) + +// func RegisterRoutes(batHandler *handler, group huma.API, mw *middleware.Middleware) { +// huma.Register(group, huma.Operation{ +// OperationID: "get-bat-runs", +// Method: http.MethodGet, +// Summary: "Get Bat Runs", +// Description: "Returns all bat runs", +// Tags: []string{"Bat"}, +// Path: "/runs", +// Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma, mw.Auth.RequireAdminHuma}, +// Errors: []int{http.StatusInternalServerError, http.StatusUnauthorized}, +// }, batHandler.handleGetRuns) + +// huma.Register(group, huma.Operation{ +// OperationID: "delete-bat-run", +// Method: http.MethodDelete, +// Summary: "Delete Bat Run", +// Description: "Delete a bat run by id", +// Tags: []string{"Bat"}, +// Path: "/runs/{runId}", +// Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma, mw.Auth.RequireAdminHuma}, +// Errors: []int{http.StatusInternalServerError, http.StatusBadRequest, http.StatusUnauthorized}, +// }, batHandler.handleDeleteRun) + +// huma.Register(group, huma.Operation{ +// OperationID: "queue-schedule-waitlist-transition-task", +// Method: http.MethodPost, +// Summary: "Queue Waitlist Transition Task", +// Description: "Queues an asynq task that transitions waitlisted applications, running every 3 days.", +// Tags: []string{"Bat"}, +// Path: "/begin-waitlist-transition", +// Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma, mw.Auth.RequireAdminHuma}, +// Errors: []int{http.StatusInternalServerError, http.StatusUnauthorized}, +// }, batHandler.handleQueueScheduleWaitlistTransitionTask) + +// huma.Register(group, huma.Operation{ +// OperationID: "queue-shutdown-waitlist-scheduler-task", +// Method: http.MethodPost, +// Summary: "Queue Shutdown Waitlist Scheduler", +// Description: "Shutsdown the scheduler used for the waitlist transition task. Error returned through logs if a scheduler is not active.", +// Tags: []string{"Bat"}, +// Path: "/shutdown-waitlist-scheduler", +// Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma, mw.Auth.RequireAdminHuma}, +// Errors: []int{http.StatusInternalServerError, http.StatusUnauthorized}, +// }, batHandler.handleQueueShutdownWaitlistSchedulerTask) +// } + +// type handler struct { +// BatService *BatService +// logger zerolog.Logger +// } + +// func NewHandler(BatService *BatService, logger zerolog.Logger) *handler { +// return &handler{ +// BatService: BatService, +// logger: logger.With().Str("handler", "BatHandler").Str("domain", "bat").Logger(), +// } +// } + +// type GetRunsOutput struct { +// Body *[]sqlc.BatRun +// } + +// func (h *handler) handleGetRuns(ctx context.Context, input *struct{}) (*GetRunsOutput, error) { +// runs, err := h.BatService.GetRuns(ctx) + +// if err != nil { +// return nil, huma.Error500InternalServerError("Failed to get bat runs") +// } + +// return &GetRunsOutput{Body: runs}, nil +// } + +// type DeleteRunOutput struct { +// Status int +// } + +// func (h *handler) handleDeleteRun(ctx context.Context, input *struct { +// RunId string `path:"runId"` +// }) (*DeleteRunOutput, error) { +// runId, err := uuid.Parse(input.RunId) + +// if err != nil { +// return nil, huma.Error400BadRequest("Invalid run id") +// } + +// err = h.BatService.DeleteRunById(ctx, runId) + +// if err != nil { +// return nil, huma.Error500InternalServerError("Failed to delete run by id") +// } + +// return &DeleteRunOutput{Status: http.StatusOK}, nil +// } + +// type QueueScheduleWaitlistTransitionTaskOutput struct { +// Status int +// } + +// func (h *handler) handleQueueScheduleWaitlistTransitionTask(ctx context.Context, input *struct{}) (*QueueScheduleWaitlistTransitionTaskOutput, error) { +// err := h.BatService.QueueScheduleWaitlistTransitionTask(ctx) + +// if err != nil { +// return nil, huma.Error500InternalServerError("Failed to queue schedule waitlist transition task") +// } + +// return &QueueScheduleWaitlistTransitionTaskOutput{Status: http.StatusOK}, nil +// } + +// type QueueShutdownWaitlistSchedulerTaskOutput struct { +// Status int +// } + +// func (h *handler) handleQueueShutdownWaitlistSchedulerTask(ctx context.Context, input *struct{}) (*QueueShutdownWaitlistSchedulerTaskOutput, error) { +// err := h.BatService.QueueShutdownWaitlistScheduler() + +// if err != nil { +// return nil, huma.Error500InternalServerError("Failed to queue shutdown waitlist scheduler task") +// } + +// return &QueueShutdownWaitlistSchedulerTaskOutput{Status: http.StatusOK}, nil +// } diff --git a/apps/api/internal/domains/bat/service.go b/apps/api/internal/domains/bat/service.go index 935457f5..b3efb330 100644 --- a/apps/api/internal/domains/bat/service.go +++ b/apps/api/internal/domains/bat/service.go @@ -1,266 +1,266 @@ package bat -import ( - "context" - "encoding/json" - "errors" - - "github.com/google/uuid" - "github.com/hibiken/asynq" - "github.com/rs/zerolog" - "github.com/swamphacks/core/apps/api/internal/config" - "github.com/swamphacks/core/apps/api/internal/database" - "github.com/swamphacks/core/apps/api/internal/database/repository" - "github.com/swamphacks/core/apps/api/internal/database/sqlc" - "github.com/swamphacks/core/apps/api/internal/domains/email" - "github.com/swamphacks/core/apps/api/internal/tasks" -) - -type BatService struct { - applicationRepo *repository.ApplicationRepository - hackathonRepo *repository.HackathonRepository - userRepo *repository.UserRepository - batRunsRepo *repository.BatRunsRepository - emailService *email.EmailService - txm *database.TransactionManager - taskQueue *asynq.Client - scheduler *asynq.Scheduler - config *config.Config - logger zerolog.Logger -} - -func NewBatService( - applicationRepo *repository.ApplicationRepository, hackathonRepo *repository.HackathonRepository, userRepo *repository.UserRepository, - batRunsRepo *repository.BatRunsRepository, emailService *email.EmailService, txm *database.TransactionManager, - taskQueue *asynq.Client, scheduler *asynq.Scheduler, config *config.Config, logger zerolog.Logger) *BatService { - return &BatService{ - taskQueue: taskQueue, - scheduler: scheduler, - applicationRepo: applicationRepo, - hackathonRepo: hackathonRepo, - userRepo: userRepo, - batRunsRepo: batRunsRepo, - emailService: emailService, - txm: txm, - config: config, - logger: logger.With().Str("service", "Bat Service").Logger(), - } -} - -var ( - ErrRunConflict = errors.New("Run already exists for this event") - ErrFailedToAddRun = errors.New("Failed to add run") -) - -func (s *BatService) AddRun(ctx context.Context) (*sqlc.BatRun, error) { - - // TODO: don't hardcode the hackathonId - run, err := s.batRunsRepo.AddRun(ctx, "xii") - if err != nil && errors.Is(err, database.ErrDuplicateRun) { - s.logger.Err(err).Msg("Could not insert result as it already exists.") - return nil, ErrRunConflict - } else if err != nil { - s.logger.Err(err).Msg("An unknown error was caught!") - return nil, ErrFailedToAddRun - } - - return run, nil -} - -func (s *BatService) GetRuns(ctx context.Context) (*[]sqlc.BatRun, error) { - return s.batRunsRepo.GetRuns(ctx) -} - -func (s *BatService) GetRunById(ctx context.Context, batId uuid.UUID) (sqlc.BatRun, error) { - return s.batRunsRepo.GetRunById(ctx, batId) -} - -func (s *BatService) UpdateRunById(ctx context.Context, params sqlc.UpdateBatRunByIdParams) (*sqlc.BatRun, error) { - err := s.batRunsRepo.UpdateRunById(ctx, params) - if err != nil { - s.logger.Err(err).Msg("update run by id failed") - return nil, err - } - - run, err := s.batRunsRepo.GetRunById(ctx, params.ID) - - return &run, err -} - -func (s *BatService) DeleteRunById(ctx context.Context, id uuid.UUID) error { - err := s.batRunsRepo.DeleteRunById(ctx, id) - if err != nil { - s.logger.Err(err).Msg("delete run by id failed") - return errors.New("Failed to delete run") - } - - return err -} - -func (s *BatService) QueueCalculateAdmissionsTask(ctx context.Context) (*asynq.TaskInfo, error) { - newRun, err := s.AddRun(ctx) - if err != nil { - return nil, ErrFailedToAddRun - } - - task, err := tasks.NewTaskCalculateAdmissions(tasks.CalculateAdmissionsPayload{ - BatRunID: newRun.ID, - }) - if err != nil { - s.logger.Err(err).Msg("Failed to create CalculateAdmissions task") - return nil, err - } - - info, err := s.taskQueue.Enqueue(task, asynq.Queue("bat")) - if err != nil { - s.logger.Err(err).Msg("Failed to queue CalculateAdmissions task") - return nil, err - } - - return info, nil -} - -func (s *BatService) CalculateAdmissions(ctx context.Context, batRunId uuid.UUID) error { - s.logger.Debug().Str("RunID", batRunId.String()).Msg("") - - // check to make sure reviews are done, update state if true, return error if not - nonReviewedApplicantUUIDs, err := s.applicationRepo.GetNonReviewedApplications(ctx) - if err != nil { - return errors.New("Could not get determine if application reviews have finished.") - } - - areReviewsComplete := len(nonReviewedApplicantUUIDs) == 0 - - if !areReviewsComplete { - return errors.New("Please make sure reviews are finished before calculating application decisions.") - } - - engine, err := NewBatEngine(0.6, 0.4) - if err != nil { - return err - } - - // Aggregate data necessary - applications, err := s.applicationRepo.ListAdmissionCandidates(ctx) - if err != nil || len(applications) == 0 { - return errors.New("Failed to retrieve applications") - } - - admissionCandidates, err := s.mapToCandidates(engine, applications) - if err != nil { - return err - } - - teams, idvs := engine.GroupCandidates(admissionCandidates) - acceptedTeamMembers, remainder := engine.AcceptTeams(teams) - - idvs = append(idvs, remainder...) - acceptedIdvs, rejected := engine.AcceptIndividuals(idvs) - - accepted := append(acceptedTeamMembers, acceptedIdvs...) - - acceptedIDs := make([]uuid.UUID, 0, len(accepted)) - rejectedIDs := make([]uuid.UUID, 0, len(rejected)) - - for _, applicant := range accepted { - acceptedIDs = append(acceptedIDs, applicant.UserID) - } - for _, applicant := range rejected { - rejectedIDs = append(rejectedIDs, applicant.UserID) - } - - params := sqlc.UpdateBatRunByIdParams{ - // TODO: add UF/other/early/late info? - AcceptedApplicantsDoUpdate: true, - RejectedApplicantsDoUpdate: true, - StatusDoUpdate: true, - AcceptedApplicants: acceptedIDs, - RejectedApplicants: rejectedIDs, - Status: sqlc.BatRunStatusCompleted, - ID: batRunId, - } - - err = s.batRunsRepo.UpdateRunById(ctx, params) - if err != nil { - return errors.New("Failed to update run") - } - - s.logger.Info().Int("Teams Members Accepted", int(len(acceptedTeamMembers))).Int("Accepted", int(engine.Quota.TotalAccepted)).Int("Rejected", len(rejected)).Msg("Finished Algo") - - return nil -} - -// This could be moved into the engine instead, or some mapping function within the bat package. -func (s *BatService) mapToCandidates(engine *BatEngine, applications []sqlc.ListAdmissionCandidatesRow) ([]AdmissionCandidate, error) { - var appAdmissionsData []AdmissionCandidate - for _, app := range applications { - if app.ExperienceRating == nil || app.PassionRating == nil { - return []AdmissionCandidate{}, errors.New("Some applications are missing their review ratings") - } - - var admissionContext AdmissionContext - if err := json.Unmarshal(app.Application, &admissionContext); err != nil { - s.logger.Debug().Bytes("App", app.Application).Msg("Application data") - return []AdmissionCandidate{}, err - } - - var teamId uuid.UUID - if app.TeamID != nil { - teamId = *app.TeamID - } - - wScore, err := engine.CalculateWeightedScore(*app.PassionRating, *app.ExperienceRating) - if err != nil { - return []AdmissionCandidate{}, err - } - appAdmissionsData = append(appAdmissionsData, AdmissionCandidate{ - UserID: app.UserID, - TeamID: uuid.NullUUID{ - UUID: teamId, - Valid: app.TeamID != nil, - }, - WeightedScore: wScore, - SortKey: 0.0, - IsUFStudent: admissionContext.School == "University of Florida", - IsEarlyCareer: admissionContext.Year == "first_year" || admissionContext.Year == "second_year", - }) - } - - return appAdmissionsData, nil -} - -func (s *BatService) QueueScheduleWaitlistTransitionTask(ctx context.Context) error { - task, err := tasks.NewTaskScheduleTransitionWaitlist(tasks.ScheduleTransitionWaitlistPayload{ - Period: s.config.AcceptFromWaitlistPeriod, - }) - if err != nil { - s.logger.Err(err).Msg("Failed to create ScheduleTransitionWaitlist task") - return err - } - - _, err = s.taskQueue.Enqueue(task, asynq.Queue("bat")) - if err != nil { - s.logger.Err(err).Msg("Failed to queue ScheduleTransitionWaitlist task") - return err - } - s.logger.Info().Msg("Queued TransitionWaitlist task") - - return nil -} - -func (s *BatService) QueueShutdownWaitlistScheduler() error { - task, err := tasks.NewTaskShutdownScheduler() - if err != nil { - s.logger.Err(err).Msg("Failed to create ShutdownWaitlistScheduler task") - return err - } - - _, err = s.taskQueue.Enqueue(task, asynq.Queue("bat")) - if err != nil { - s.logger.Err(err).Msg("Failed to queue ShutdownWaitlistScheduler task") - return err - } - s.logger.Info().Msg("Queued ShutdownWaitlistScheduler task") - - return nil -} +// import ( +// "context" +// "encoding/json" +// "errors" + +// "github.com/google/uuid" +// "github.com/hibiken/asynq" +// "github.com/rs/zerolog" +// "github.com/swamphacks/core/apps/api/internal/config" +// "github.com/swamphacks/core/apps/api/internal/database" +// "github.com/swamphacks/core/apps/api/internal/database/repository" +// "github.com/swamphacks/core/apps/api/internal/database/sqlc" +// "github.com/swamphacks/core/apps/api/internal/domains/email" +// "github.com/swamphacks/core/apps/api/internal/tasks" +// ) + +// type BatService struct { +// applicationRepo *repository.ApplicationRepository +// hackathonRepo *repository.HackathonRepository +// userRepo *repository.UserRepository +// batRunsRepo *repository.BatRunsRepository +// emailService *email.EmailService +// txm *database.TransactionManager +// taskQueue *asynq.Client +// scheduler *asynq.Scheduler +// config *config.Config +// logger zerolog.Logger +// } + +// func NewBatService( +// applicationRepo *repository.ApplicationRepository, hackathonRepo *repository.HackathonRepository, userRepo *repository.UserRepository, +// batRunsRepo *repository.BatRunsRepository, emailService *email.EmailService, txm *database.TransactionManager, +// taskQueue *asynq.Client, scheduler *asynq.Scheduler, config *config.Config, logger zerolog.Logger) *BatService { +// return &BatService{ +// taskQueue: taskQueue, +// scheduler: scheduler, +// applicationRepo: applicationRepo, +// hackathonRepo: hackathonRepo, +// userRepo: userRepo, +// batRunsRepo: batRunsRepo, +// emailService: emailService, +// txm: txm, +// config: config, +// logger: logger.With().Str("service", "Bat Service").Logger(), +// } +// } + +// var ( +// ErrRunConflict = errors.New("Run already exists for this event") +// ErrFailedToAddRun = errors.New("Failed to add run") +// ) + +// func (s *BatService) AddRun(ctx context.Context) (*sqlc.BatRun, error) { + +// // TODO: don't hardcode the hackathonId +// run, err := s.batRunsRepo.AddRun(ctx, "xii") +// if err != nil && errors.Is(err, database.ErrDuplicateRun) { +// s.logger.Err(err).Msg("Could not insert result as it already exists.") +// return nil, ErrRunConflict +// } else if err != nil { +// s.logger.Err(err).Msg("An unknown error was caught!") +// return nil, ErrFailedToAddRun +// } + +// return run, nil +// } + +// func (s *BatService) GetRuns(ctx context.Context) (*[]sqlc.BatRun, error) { +// return s.batRunsRepo.GetRuns(ctx) +// } + +// func (s *BatService) GetRunById(ctx context.Context, batId uuid.UUID) (sqlc.BatRun, error) { +// return s.batRunsRepo.GetRunById(ctx, batId) +// } + +// func (s *BatService) UpdateRunById(ctx context.Context, params sqlc.UpdateBatRunByIdParams) (*sqlc.BatRun, error) { +// err := s.batRunsRepo.UpdateRunById(ctx, params) +// if err != nil { +// s.logger.Err(err).Msg("update run by id failed") +// return nil, err +// } + +// run, err := s.batRunsRepo.GetRunById(ctx, params.ID) + +// return &run, err +// } + +// func (s *BatService) DeleteRunById(ctx context.Context, id uuid.UUID) error { +// err := s.batRunsRepo.DeleteRunById(ctx, id) +// if err != nil { +// s.logger.Err(err).Msg("delete run by id failed") +// return errors.New("Failed to delete run") +// } + +// return err +// } + +// func (s *BatService) QueueCalculateAdmissionsTask(ctx context.Context) (*asynq.TaskInfo, error) { +// newRun, err := s.AddRun(ctx) +// if err != nil { +// return nil, ErrFailedToAddRun +// } + +// task, err := tasks.NewTaskCalculateAdmissions(tasks.CalculateAdmissionsPayload{ +// BatRunID: newRun.ID, +// }) +// if err != nil { +// s.logger.Err(err).Msg("Failed to create CalculateAdmissions task") +// return nil, err +// } + +// info, err := s.taskQueue.Enqueue(task, asynq.Queue("bat")) +// if err != nil { +// s.logger.Err(err).Msg("Failed to queue CalculateAdmissions task") +// return nil, err +// } + +// return info, nil +// } + +// func (s *BatService) CalculateAdmissions(ctx context.Context, batRunId uuid.UUID) error { +// s.logger.Debug().Str("RunID", batRunId.String()).Msg("") + +// // check to make sure reviews are done, update state if true, return error if not +// nonReviewedApplicantUUIDs, err := s.applicationRepo.GetNonReviewedApplications(ctx) +// if err != nil { +// return errors.New("Could not get determine if application reviews have finished.") +// } + +// areReviewsComplete := len(nonReviewedApplicantUUIDs) == 0 + +// if !areReviewsComplete { +// return errors.New("Please make sure reviews are finished before calculating application decisions.") +// } + +// engine, err := NewBatEngine(0.6, 0.4) +// if err != nil { +// return err +// } + +// // Aggregate data necessary +// applications, err := s.applicationRepo.ListAdmissionCandidates(ctx) +// if err != nil || len(applications) == 0 { +// return errors.New("Failed to retrieve applications") +// } + +// admissionCandidates, err := s.mapToCandidates(engine, applications) +// if err != nil { +// return err +// } + +// teams, idvs := engine.GroupCandidates(admissionCandidates) +// acceptedTeamMembers, remainder := engine.AcceptTeams(teams) + +// idvs = append(idvs, remainder...) +// acceptedIdvs, rejected := engine.AcceptIndividuals(idvs) + +// accepted := append(acceptedTeamMembers, acceptedIdvs...) + +// acceptedIDs := make([]uuid.UUID, 0, len(accepted)) +// rejectedIDs := make([]uuid.UUID, 0, len(rejected)) + +// for _, applicant := range accepted { +// acceptedIDs = append(acceptedIDs, applicant.UserID) +// } +// for _, applicant := range rejected { +// rejectedIDs = append(rejectedIDs, applicant.UserID) +// } + +// params := sqlc.UpdateBatRunByIdParams{ +// // TODO: add UF/other/early/late info? +// AcceptedApplicantsDoUpdate: true, +// RejectedApplicantsDoUpdate: true, +// StatusDoUpdate: true, +// AcceptedApplicants: acceptedIDs, +// RejectedApplicants: rejectedIDs, +// Status: sqlc.BatRunStatusCompleted, +// ID: batRunId, +// } + +// err = s.batRunsRepo.UpdateRunById(ctx, params) +// if err != nil { +// return errors.New("Failed to update run") +// } + +// s.logger.Info().Int("Teams Members Accepted", int(len(acceptedTeamMembers))).Int("Accepted", int(engine.Quota.TotalAccepted)).Int("Rejected", len(rejected)).Msg("Finished Algo") + +// return nil +// } + +// // This could be moved into the engine instead, or some mapping function within the bat package. +// func (s *BatService) mapToCandidates(engine *BatEngine, applications []sqlc.ListAdmissionCandidatesRow) ([]AdmissionCandidate, error) { +// var appAdmissionsData []AdmissionCandidate +// for _, app := range applications { +// if app.ExperienceRating == nil || app.PassionRating == nil { +// return []AdmissionCandidate{}, errors.New("Some applications are missing their review ratings") +// } + +// var admissionContext AdmissionContext +// if err := json.Unmarshal(app.Application, &admissionContext); err != nil { +// s.logger.Debug().Bytes("App", app.Application).Msg("Application data") +// return []AdmissionCandidate{}, err +// } + +// var teamId uuid.UUID +// if app.TeamID != nil { +// teamId = *app.TeamID +// } + +// wScore, err := engine.CalculateWeightedScore(*app.PassionRating, *app.ExperienceRating) +// if err != nil { +// return []AdmissionCandidate{}, err +// } +// appAdmissionsData = append(appAdmissionsData, AdmissionCandidate{ +// UserID: app.UserID, +// TeamID: uuid.NullUUID{ +// UUID: teamId, +// Valid: app.TeamID != nil, +// }, +// WeightedScore: wScore, +// SortKey: 0.0, +// IsUFStudent: admissionContext.School == "University of Florida", +// IsEarlyCareer: admissionContext.Year == "first_year" || admissionContext.Year == "second_year", +// }) +// } + +// return appAdmissionsData, nil +// } + +// func (s *BatService) QueueScheduleWaitlistTransitionTask(ctx context.Context) error { +// task, err := tasks.NewTaskScheduleTransitionWaitlist(tasks.ScheduleTransitionWaitlistPayload{ +// Period: s.config.AcceptFromWaitlistPeriod, +// }) +// if err != nil { +// s.logger.Err(err).Msg("Failed to create ScheduleTransitionWaitlist task") +// return err +// } + +// _, err = s.taskQueue.Enqueue(task, asynq.Queue("bat")) +// if err != nil { +// s.logger.Err(err).Msg("Failed to queue ScheduleTransitionWaitlist task") +// return err +// } +// s.logger.Info().Msg("Queued TransitionWaitlist task") + +// return nil +// } + +// func (s *BatService) QueueShutdownWaitlistScheduler() error { +// task, err := tasks.NewTaskShutdownScheduler() +// if err != nil { +// s.logger.Err(err).Msg("Failed to create ShutdownWaitlistScheduler task") +// return err +// } + +// _, err = s.taskQueue.Enqueue(task, asynq.Queue("bat")) +// if err != nil { +// s.logger.Err(err).Msg("Failed to queue ShutdownWaitlistScheduler task") +// return err +// } +// s.logger.Info().Msg("Queued ShutdownWaitlistScheduler task") + +// return nil +// } diff --git a/apps/api/internal/domains/email/campaign_http.go b/apps/api/internal/domains/email/campaign_http.go new file mode 100644 index 00000000..ca9eba7a --- /dev/null +++ b/apps/api/internal/domains/email/campaign_http.go @@ -0,0 +1,318 @@ +package email + +import ( + "context" + "errors" + "net/http" + "time" + + "github.com/danielgtaylor/huma/v2" + "github.com/google/uuid" + "github.com/rs/zerolog" + "github.com/swamphacks/core/apps/api/internal/api/cookie" + "github.com/swamphacks/core/apps/api/internal/api/middleware" + "github.com/swamphacks/core/apps/api/internal/ctxutils" + "github.com/swamphacks/core/apps/api/internal/database/sqlc" +) + +func RegisterCampaignRoutes(emailCampaignHandler *emailCampaignHandler, group huma.API, mw *middleware.Middleware) { + huma.Register(group, huma.Operation{ + OperationID: "create-email-campaign", + Method: http.MethodPost, + Summary: "Create Email Campaign", + Description: "Creates a saved email campaign draft for a hackathon.", + Tags: []string{"Email Campaigns"}, + Path: "/campaigns", + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma, mw.Auth.RequireAdminHuma}, + Errors: []int{http.StatusUnauthorized, http.StatusBadRequest, http.StatusInternalServerError}, + Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, + DefaultStatus: http.StatusCreated, + }, emailCampaignHandler.handleCreateCampaign) + + huma.Register(group, huma.Operation{ + OperationID: "list-email-campaigns", + Method: http.MethodGet, + Summary: "List Email Campaigns", + Description: "Returns all saved email campaigns for a hackathon.", + Tags: []string{"Email Campaigns"}, + Path: "/campaigns", + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma, mw.Auth.RequireAdminHuma}, + Errors: []int{http.StatusUnauthorized, http.StatusBadRequest, http.StatusInternalServerError}, + Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, + DefaultStatus: http.StatusOK, + }, emailCampaignHandler.handleListCampaigns) + + huma.Register(group, huma.Operation{ + OperationID: "get-email-campaign", + Method: http.MethodGet, + Summary: "Get Email Campaign", + Description: "Returns one saved email campaign by id.", + Tags: []string{"Email Campaigns"}, + Path: "/campaigns/{campaignId}", + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma, mw.Auth.RequireAdminHuma}, + Errors: []int{http.StatusUnauthorized, http.StatusBadRequest, http.StatusNotFound, http.StatusInternalServerError}, + Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, + DefaultStatus: http.StatusOK, + }, emailCampaignHandler.handleGetCampaign) + + huma.Register(group, huma.Operation{ + OperationID: "update-email-campaign", + Method: http.MethodPatch, + Summary: "Update Email Campaign", + Description: "Updates editable fields on a draft or scheduled email campaign.", + Tags: []string{"Email Campaigns"}, + Path: "/campaigns/{campaignId}", + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma, mw.Auth.RequireAdminHuma}, + Errors: []int{http.StatusUnauthorized, http.StatusBadRequest, http.StatusNotFound, http.StatusInternalServerError}, + Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, + DefaultStatus: http.StatusOK, + }, emailCampaignHandler.handleUpdateCampaign) + + huma.Register(group, huma.Operation{ + OperationID: "update-email-campaign-status", + Method: http.MethodPatch, + Summary: "Update Email Campaign Status", + Description: "Updates lifecycle fields such as status, scheduled_at, sent_at, and last_error.", + Tags: []string{"Email Campaigns"}, + Path: "/campaigns/{campaignId}/status", + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma, mw.Auth.RequireAdminHuma}, + Errors: []int{http.StatusUnauthorized, http.StatusBadRequest, http.StatusNotFound, http.StatusInternalServerError}, + Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, + DefaultStatus: http.StatusOK, + }, emailCampaignHandler.handleUpdateCampaignStatus) +} + +type emailCampaignHandler struct { + emailCampaignService *EmailCampaignService + logger zerolog.Logger +} + +func NewCampaignHandler(emailCampaignService *EmailCampaignService, logger zerolog.Logger) *emailCampaignHandler { + return &emailCampaignHandler{ + emailCampaignService: emailCampaignService, + logger: logger.With().Str("handler", "EmailCampaignHandler").Str("domain", "email").Logger(), + } +} + +type CreateEmailCampaignRequest struct { + HackathonID string `json:"hackathonId" required:"true"` + Title string `json:"title" minLength:"1"` + Description *string `json:"description,omitempty"` + Subject string `json:"subject" minLength:"1"` + Body string `json:"body" minLength:"1"` + Format sqlc.EmailCampaignFormat `json:"format" required:"true"` + RecipientTypes []string `json:"recipientTypes" minItems:"1"` + ScheduledAt *time.Time `json:"scheduledAt,omitempty"` +} + +type UpdateEmailCampaignRequest struct { + Title *string `json:"title,omitempty"` + Description *string `json:"description,omitempty"` + Subject *string `json:"subject,omitempty"` + Body *string `json:"body,omitempty"` + Format *sqlc.EmailCampaignFormat `json:"format,omitempty"` + RecipientTypes *[]string `json:"recipientTypes,omitempty"` + ScheduledAt *time.Time `json:"scheduledAt,omitempty"` +} + +type UpdateEmailCampaignStatusRequest struct { + Status sqlc.EmailCampaignStatus `json:"status" required:"true"` + ScheduledAt *time.Time `json:"scheduledAt,omitempty"` + SentAt *time.Time `json:"sentAt,omitempty"` + LastError *string `json:"lastError,omitempty"` +} + +type EmailCampaignOutput struct { + Body *sqlc.EmailCampaign +} + +type ListEmailCampaignsOutput struct { + Body []sqlc.EmailCampaign +} + +func (h *emailCampaignHandler) handleCreateCampaign(ctx context.Context, input *struct { + Body CreateEmailCampaignRequest +}) (*EmailCampaignOutput, error) { + userCtx := ctxutils.GetUserFromCtx(ctx) + if userCtx == nil { + return nil, huma.Error400BadRequest("Failed to get current user info") + } + + campaign, err := h.emailCampaignService.CreateCampaign(ctx, sqlc.CreateEmailCampaignParams{ + HackathonID: input.Body.HackathonID, + Title: input.Body.Title, + Description: input.Body.Description, + Subject: input.Body.Subject, + Body: input.Body.Body, + Format: input.Body.Format, + RecipientTypes: input.Body.RecipientTypes, + ScheduledAt: input.Body.ScheduledAt, + CreatedByUserID: &userCtx.UserID, + UpdatedByUserID: &userCtx.UserID, + }) + if err != nil { + h.logger.Err(err).Msg("Failed to create email campaign") + return nil, campaignHTTPError(err, "Failed to create email campaign") + } + + return &EmailCampaignOutput{Body: campaign}, nil +} + +func (h *emailCampaignHandler) handleListCampaigns(ctx context.Context, input *struct { + HackathonID string `query:"hackathonId" required:"true"` +}) (*ListEmailCampaignsOutput, error) { + campaigns, err := h.emailCampaignService.ListCampaigns(ctx, input.HackathonID) + if err != nil { + return nil, campaignHTTPError(err, "Failed to list email campaigns") + } + + return &ListEmailCampaignsOutput{Body: campaigns}, nil +} + +func (h *emailCampaignHandler) handleGetCampaign(ctx context.Context, input *struct { + CampaignID string `path:"campaignId"` + HackathonID string `query:"hackathonId" required:"true"` +}) (*EmailCampaignOutput, error) { + campaignID, err := uuid.Parse(input.CampaignID) + if err != nil { + return nil, huma.Error400BadRequest("Invalid campaign id") + } + + campaign, err := h.emailCampaignService.GetCampaignByID(ctx, sqlc.GetEmailCampaignByIDParams{ + ID: campaignID, + HackathonID: input.HackathonID, + }) + if err != nil { + return nil, campaignHTTPError(err, "Failed to get email campaign") + } + + return &EmailCampaignOutput{Body: campaign}, nil +} + +func (h *emailCampaignHandler) handleUpdateCampaign(ctx context.Context, input *struct { + CampaignID string `path:"campaignId"` + HackathonID string `query:"hackathonId" required:"true"` + Body UpdateEmailCampaignRequest +}) (*EmailCampaignOutput, error) { + userCtx := ctxutils.GetUserFromCtx(ctx) + if userCtx == nil { + return nil, huma.Error400BadRequest("Failed to get current user info") + } + + campaignID, err := uuid.Parse(input.CampaignID) + if err != nil { + return nil, huma.Error400BadRequest("Invalid campaign id") + } + if input.Body.Title != nil && *input.Body.Title == "" { + return nil, huma.Error400BadRequest(ErrEmailCampaignTitleRequired.Error()) + } + if input.Body.Subject != nil && *input.Body.Subject == "" { + return nil, huma.Error400BadRequest(ErrEmailCampaignSubjectRequired.Error()) + } + if input.Body.Body != nil && *input.Body.Body == "" { + return nil, huma.Error400BadRequest(ErrEmailCampaignBodyRequired.Error()) + } + if input.Body.RecipientTypes != nil && len(*input.Body.RecipientTypes) == 0 { + return nil, huma.Error400BadRequest(ErrEmailCampaignRecipientsRequired.Error()) + } + + params := sqlc.UpdateEmailCampaignParams{ + TitleDoUpdate: input.Body.Title != nil, + DescriptionDoUpdate: input.Body.Description != nil, + SubjectDoUpdate: input.Body.Subject != nil, + BodyDoUpdate: input.Body.Body != nil, + FormatDoUpdate: input.Body.Format != nil, + RecipientTypesDoUpdate: input.Body.RecipientTypes != nil, + ScheduledAtDoUpdate: input.Body.ScheduledAt != nil, + UpdatedByUserIDDoUpdate: true, + UpdatedByUserID: userCtx.UserID, + ID: campaignID, + HackathonID: input.HackathonID, + } + + if input.Body.Title != nil { + params.Title = *input.Body.Title + } + if input.Body.Description != nil { + params.Description = input.Body.Description + } + if input.Body.Subject != nil { + params.Subject = *input.Body.Subject + } + if input.Body.Body != nil { + params.Body = *input.Body.Body + } + if input.Body.Format != nil { + params.Format = sqlc.NullEmailCampaignFormat{ + EmailCampaignFormat: *input.Body.Format, + Valid: true, + } + } + if input.Body.RecipientTypes != nil { + params.RecipientTypes = *input.Body.RecipientTypes + } + if input.Body.ScheduledAt != nil { + params.ScheduledAt = input.Body.ScheduledAt + } + + campaign, err := h.emailCampaignService.UpdateCampaign(ctx, params) + if err != nil { + h.logger.Err(err).Msg("Failed to update email campaign") + return nil, campaignHTTPError(err, "Failed to update email campaign") + } + + return &EmailCampaignOutput{Body: campaign}, nil +} + +func (h *emailCampaignHandler) handleUpdateCampaignStatus(ctx context.Context, input *struct { + CampaignID string `path:"campaignId"` + HackathonID string `query:"hackathonId" required:"true"` + Body UpdateEmailCampaignStatusRequest +}) (*EmailCampaignOutput, error) { + userCtx := ctxutils.GetUserFromCtx(ctx) + if userCtx == nil { + return nil, huma.Error400BadRequest("Failed to get current user info") + } + + campaignID, err := uuid.Parse(input.CampaignID) + if err != nil { + return nil, huma.Error400BadRequest("Invalid campaign id") + } + + campaign, err := h.emailCampaignService.UpdateCampaignStatus(ctx, sqlc.UpdateEmailCampaignStatusParams{ + Status: input.Body.Status, + ScheduledAtDoUpdate: input.Body.ScheduledAt != nil, + ScheduledAt: input.Body.ScheduledAt, + SentAtDoUpdate: input.Body.SentAt != nil, + SentAt: input.Body.SentAt, + LastErrorDoUpdate: input.Body.LastError != nil, + LastError: input.Body.LastError, + UpdatedByUserIDDoUpdate: true, + UpdatedByUserID: userCtx.UserID, + ID: campaignID, + HackathonID: input.HackathonID, + }) + if err != nil { + return nil, campaignHTTPError(err, "Failed to update email campaign status") + } + + return &EmailCampaignOutput{Body: campaign}, nil +} + +func campaignHTTPError(err error, fallback string) error { + if errors.Is(err, ErrEmailCampaignNotFound) { + return huma.Error404NotFound("Email campaign not found") + } + + if errors.Is(err, ErrEmailCampaignCannotEdit) || + errors.Is(err, ErrEmailCampaignTitleRequired) || + errors.Is(err, ErrEmailCampaignSubjectRequired) || + errors.Is(err, ErrEmailCampaignBodyRequired) || + errors.Is(err, ErrEmailCampaignRecipientsRequired) || + errors.Is(err, ErrEmailCampaignScheduledAtRequired) || + errors.Is(err, ErrEmailCampaignSentAtRequired) { + return huma.Error400BadRequest(err.Error()) + } + + return huma.Error500InternalServerError(fallback) +} diff --git a/apps/api/internal/domains/email/campaign_service.go b/apps/api/internal/domains/email/campaign_service.go new file mode 100644 index 00000000..30e628ff --- /dev/null +++ b/apps/api/internal/domains/email/campaign_service.go @@ -0,0 +1,149 @@ +package email + +import ( + "context" + "errors" + "strings" + + "github.com/rs/zerolog" + "github.com/swamphacks/core/apps/api/internal/database/repository" + "github.com/swamphacks/core/apps/api/internal/database/sqlc" +) + +var ( + //Reuses repository-level "not found" error + ErrEmailCampaignNotFound = repository.ErrEmailCampaignNotFound + + //validation errors before writing error to db + ErrEmailCampaignTitleRequired = errors.New("email campaign title is required") + ErrEmailCampaignSubjectRequired = errors.New("email campaign subject is required") + ErrEmailCampaignBodyRequired = errors.New("email campaign body is required") + ErrEmailCampaignRecipientsRequired = errors.New("email campaign recipients are required") + + ErrEmailCampaignCannotEdit = errors.New("email campaign cannot be edited") + + //status-specific validation errors + ErrEmailCampaignScheduledAtRequired = errors.New("scheduled_at is required for scheduled campaigns") + ErrEmailCampaignSentAtRequired = errors.New("sent_at is required for sent campaigns") +) + +// EmailCampaignService owns business rules for saved email campaigns. +type EmailCampaignService struct { + emailCampaignRepo *repository.EmailCampaignRepository + logger zerolog.Logger +} + +// NewEmailCampaignService creates the service and stores its dependencies. +// This will eventually be called from api.go when wiring the app together. +func NewEmailCampaignService( + emailCampaignRepo *repository.EmailCampaignRepository, + logger zerolog.Logger, +) *EmailCampaignService { + return &EmailCampaignService{ + emailCampaignRepo: emailCampaignRepo, + logger: logger.With().Str("service", "EmailCampaignService").Str("domain", "email").Logger(), + } +} + +// CreateCampaign validates required campaign fields, then stores a new campaign. +// The actual INSERT is handled by the repository/sqlc layer. +func (s *EmailCampaignService) CreateCampaign( + ctx context.Context, + params sqlc.CreateEmailCampaignParams, +) (*sqlc.EmailCampaign, error) { + if err := validateCampaignContent(params.Title, params.Subject, params.Body, params.RecipientTypes); err != nil { + return nil, err + } + + return s.emailCampaignRepo.CreateEmailCampaign(ctx, params) +} + +// GetCampaignByID fetches one campaign scoped to a hackathon. +// The hackathon scope prevents one event from reading another event's campaign. +func (s *EmailCampaignService) GetCampaignByID( + ctx context.Context, + params sqlc.GetEmailCampaignByIDParams, +) (*sqlc.EmailCampaign, error) { + return s.emailCampaignRepo.GetEmailCampaignByID(ctx, params) +} + +// ListCampaigns fetches all campaigns for one hackathon. +// Sorting is handled by the SQL query, currently newest first. +func (s *EmailCampaignService) ListCampaigns( + ctx context.Context, + hackathonID string, +) ([]sqlc.EmailCampaign, error) { + return s.emailCampaignRepo.ListEmailCampaigns(ctx, hackathonID) +} + +// UpdateCampaign updates editable campaign fields. +// It first loads the existing campaign so we can enforce status rules before updating. +func (s *EmailCampaignService) UpdateCampaign( + ctx context.Context, + params sqlc.UpdateEmailCampaignParams, +) (*sqlc.EmailCampaign, error) { + existingCampaign, err := s.emailCampaignRepo.GetEmailCampaignByID(ctx, sqlc.GetEmailCampaignByIDParams{ + ID: params.ID, + HackathonID: params.HackathonID, + }) + if err != nil { + return nil, err + } + + if !canEditCampaign(existingCampaign.Status) { + return nil, ErrEmailCampaignCannotEdit + } + + return s.emailCampaignRepo.UpdateEmailCampaign(ctx, params) +} + +// UpdateCampaignStatus changes lifecycle fields such as draft -> scheduled or sending -> sent. +// The database also has constraints, but checking here gives cleaner service-level errors. +func (s *EmailCampaignService) UpdateCampaignStatus( + ctx context.Context, + params sqlc.UpdateEmailCampaignStatusParams, +) (*sqlc.EmailCampaign, error) { + if params.Status == sqlc.EmailCampaignStatusScheduled && params.ScheduledAt == nil { + return nil, ErrEmailCampaignScheduledAtRequired + } + + if params.Status == sqlc.EmailCampaignStatusSent && params.SentAt == nil { + return nil, ErrEmailCampaignSentAtRequired + } + + return s.emailCampaignRepo.UpdateEmailCampaignStatus(ctx, params) +} + +// validateCampaignContent checks fields that every campaign needs before it is saved. +// strings.TrimSpace prevents values like " " from passing validation. +func validateCampaignContent( + title string, + subject string, + body string, + recipientTypes []string, +) error { + if strings.TrimSpace(title) == "" { + return ErrEmailCampaignTitleRequired + } + + if strings.TrimSpace(subject) == "" { + return ErrEmailCampaignSubjectRequired + } + + if strings.TrimSpace(body) == "" { + return ErrEmailCampaignBodyRequired + } + + if len(recipientTypes) == 0 { + return ErrEmailCampaignRecipientsRequired + } + + return nil +} + +// canEditCampaign centralizes edit rules. +// Drafts are editable, and scheduled campaigns can still be adjusted before sending. +func canEditCampaign(status sqlc.EmailCampaignStatus) bool { + return status == sqlc.EmailCampaignStatusDraft || + status == sqlc.EmailCampaignStatusScheduled +} diff --git a/apps/api/internal/domains/email/campaign_service_test.go b/apps/api/internal/domains/email/campaign_service_test.go new file mode 100644 index 00000000..d1a309b8 --- /dev/null +++ b/apps/api/internal/domains/email/campaign_service_test.go @@ -0,0 +1,139 @@ +package email + +import ( + "context" + "errors" + "testing" + + "github.com/swamphacks/core/apps/api/internal/database/sqlc" +) + +func TestValidateCampaignContent(t *testing.T) { + tests := []struct { + name string + title string + subject string + body string + recipientTypes []string + expectedError error + }{ + { + name: "valid campaign", + title: "Welcome", + subject: "Welcome to SwampHacks", + body: "Campaign body", + recipientTypes: []string{"admins"}, + expectedError: nil, + }, + { + name: "missing title", + title: " ", + subject: "Subject", + body: "Body", + recipientTypes: []string{"admins"}, + expectedError: ErrEmailCampaignTitleRequired, + }, + { + name: "missing subject", + title: "Title", + subject: "", + body: "Body", + recipientTypes: []string{"admins"}, + expectedError: ErrEmailCampaignSubjectRequired, + }, + { + name: "missing body", + title: "Title", + subject: "Subject", + body: " ", + recipientTypes: []string{"admins"}, + expectedError: ErrEmailCampaignBodyRequired, + }, + { + name: "missing recipients", + title: "Title", + subject: "Subject", + body: "Body", + recipientTypes: nil, + expectedError: ErrEmailCampaignRecipientsRequired, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := validateCampaignContent( + test.title, + test.subject, + test.body, + test.recipientTypes, + ) + + if !errors.Is(err, test.expectedError) { + t.Fatalf("expected error %v, got %v", test.expectedError, err) + } + }) + } +} + +func TestCanEditCampaign(t *testing.T) { + tests := []struct { + status sqlc.EmailCampaignStatus + expected bool + }{ + {status: sqlc.EmailCampaignStatusDraft, expected: true}, + {status: sqlc.EmailCampaignStatusScheduled, expected: true}, + {status: sqlc.EmailCampaignStatusSending, expected: false}, + {status: sqlc.EmailCampaignStatusSent, expected: false}, + {status: sqlc.EmailCampaignStatusFailed, expected: false}, + } + + for _, test := range tests { + t.Run(string(test.status), func(t *testing.T) { + result := canEditCampaign(test.status) + + if result != test.expected { + t.Fatalf("expected %v, got %v", test.expected, result) + } + }) + } +} + +func TestUpdateCampaignStatusRequiresScheduledAt(t *testing.T) { + service := &EmailCampaignService{} + + _, err := service.UpdateCampaignStatus( + context.Background(), + sqlc.UpdateEmailCampaignStatusParams{ + Status: sqlc.EmailCampaignStatusScheduled, + ScheduledAt: nil, + }, + ) + + if !errors.Is(err, ErrEmailCampaignScheduledAtRequired) { + t.Fatalf( + "expected %v, got %v", + ErrEmailCampaignScheduledAtRequired, + err, + ) + } +} + +func TestUpdateCampaignStatusRequiresSentAt(t *testing.T) { + service := &EmailCampaignService{} + + _, err := service.UpdateCampaignStatus( + context.Background(), + sqlc.UpdateEmailCampaignStatusParams{ + Status: sqlc.EmailCampaignStatusSent, + SentAt: nil, + }, + ) + + if !errors.Is(err, ErrEmailCampaignSentAtRequired) { + t.Fatalf( + "expected %v, got %v", + ErrEmailCampaignSentAtRequired, + err, + ) + } +} \ No newline at end of file diff --git a/apps/api/internal/domains/hackathon/http.go b/apps/api/internal/domains/hackathon/http.go index 3ad87ef3..613e47f7 100644 --- a/apps/api/internal/domains/hackathon/http.go +++ b/apps/api/internal/domains/hackathon/http.go @@ -305,7 +305,7 @@ func (h *handler) handleUpdateHackathon(ctx context.Context, input *struct { } type GetStaffOutput struct { - Body []sqlc.User + Body []sqlc.User `json:"body" nullable:"false"` } func (h *handler) handleGetStaff(ctx context.Context, input *struct{}) (*GetStaffOutput, error) { diff --git a/apps/api/internal/domains/hackathon/service.go b/apps/api/internal/domains/hackathon/service.go index 10eddea1..51789858 100644 --- a/apps/api/internal/domains/hackathon/service.go +++ b/apps/api/internal/domains/hackathon/service.go @@ -154,9 +154,6 @@ func (s *HackathonService) CheckInAttendee(ctx context.Context, userID uuid.UUID return s.userRepo.UpdateUser(ctx, sqlc.UpdateUserParams{ ID: userID, - Role: sqlc.UserRoleAttendee, - RoleDoUpdate: true, - CheckedInAt: &now, CheckedInAtDoUpdate: true, diff --git a/apps/api/internal/domains/users/http.go b/apps/api/internal/domains/users/http.go index b968ccb8..d26b9384 100644 --- a/apps/api/internal/domains/users/http.go +++ b/apps/api/internal/domains/users/http.go @@ -151,6 +151,18 @@ func RegisterRoutes(userHandler *handler, group huma.API, mw *middleware.Middlew Errors: []int{http.StatusUnauthorized, http.StatusBadRequest, http.StatusInternalServerError}, Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, }, userHandler.handleRevokeEventRole) + + huma.Register(group, huma.Operation{ + OperationID: "update-has-seen-new-application-status", + Method: http.MethodPost, + Summary: "Acknowledge New Application Status", + Description: "Mark that the user has seen their new application status", + Tags: []string{"Users"}, + Path: "/me/acknowledge-new-application-status", + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma}, + Errors: []int{http.StatusUnauthorized, http.StatusBadRequest, http.StatusInternalServerError}, + Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, + }, userHandler.handleAcknowledgeNewApplicationStatus) } type handler struct { @@ -277,16 +289,22 @@ func (h *handler) handleUpdateUser(ctx context.Context, input *struct { return nil, huma.Error400BadRequest("Invalid email format") } + var preferredEmail *string + + if input.Body.PreferredEmail != "" { + preferredEmail = &input.Body.PreferredEmail + } + // TODO: Allow/add more fields here params := sqlc.UpdateUserParams{ ID: userCtx.UserID, NameDoUpdate: true, Name: input.Body.Name, PreferredEmailDoUpdate: true, - PreferredEmail: &input.Body.PreferredEmail, + PreferredEmail: preferredEmail, } - err := h.userService.UpdateUser(ctx, userCtx.UserID, params) + err := h.userService.UpdateUser(ctx, params) if err != nil { h.logger.Err(err).Msg("failed to update user") if errors.Is(err, ErrUserNotFound) { @@ -321,11 +339,12 @@ func (h *handler) handleUpdateEmailConsent(ctx context.Context, input *struct { } params := sqlc.UpdateUserParams{ + ID: userCtx.UserID, EmailConsentDoUpdate: true, EmailConsent: input.Body.EmailConsent, } - err := h.userService.UpdateUser(ctx, userCtx.UserID, params) + err := h.userService.UpdateUser(ctx, params) if err != nil { h.logger.Err(err).Msg("failed to update email consent") @@ -496,6 +515,30 @@ func (h *handler) handleRevokeEventRole(ctx context.Context, input *struct { return &RevokeEventRoleOutput{Status: http.StatusOK}, nil } +type AcknowledgeNewApplicationStatusOutput struct { + Status int +} + +func (h *handler) handleAcknowledgeNewApplicationStatus(ctx context.Context, input *struct{}) (*AcknowledgeNewApplicationStatusOutput, error) { + userCtx := ctxutils.GetUserFromCtx(ctx) + + if userCtx == nil { + return nil, huma.Error400BadRequest("Failed to get current user info") + } + + err := h.userService.UpdateUser(ctx, sqlc.UpdateUserParams{ + ID: userCtx.UserID, + HasSeenNewApplicationStatusDoUpdate: true, + HasSeenNewApplicationStatus: new(true), + }) + + if err != nil { + return nil, huma.Error500InternalServerError("Failed to acknowledge new application status") + } + + return &AcknowledgeNewApplicationStatusOutput{Status: http.StatusOK}, nil +} + func ParseUUIDOrNil(s *string) *uuid.UUID { if s == nil || *s == "" { return nil diff --git a/apps/api/internal/domains/users/service.go b/apps/api/internal/domains/users/service.go index 792cc822..65d67311 100644 --- a/apps/api/internal/domains/users/service.go +++ b/apps/api/internal/domains/users/service.go @@ -99,9 +99,7 @@ func (s *UserService) GetUserByRFID(ctx context.Context, rfid string) (*sqlc.Use // return checkedIn, nil // } -func (s *UserService) UpdateUser(ctx context.Context, userID uuid.UUID, params sqlc.UpdateUserParams) error { - params.ID = userID - +func (s *UserService) UpdateUser(ctx context.Context, params sqlc.UpdateUserParams) error { err := s.userRepo.UpdateUser(ctx, params) if err != nil { if err == repository.ErrUserNotFound { @@ -127,7 +125,7 @@ func (s *UserService) CompleteOnboarding(ctx context.Context, userID uuid.UUID, Onboarded: true, } - return s.UpdateUser(ctx, userID, params) + return s.UpdateUser(ctx, params) } func (s *UserService) GetAllUsers(ctx context.Context, search *string, limit, offset int32) ([]sqlc.User, error) { diff --git a/apps/api/internal/workers/bat.go b/apps/api/internal/workers/bat.go index d84ac8a6..f8a747a4 100644 --- a/apps/api/internal/workers/bat.go +++ b/apps/api/internal/workers/bat.go @@ -1,138 +1,138 @@ package workers -import ( - "context" - "encoding/json" - "errors" - "time" - - "github.com/hibiken/asynq" - "github.com/rs/zerolog" - "github.com/swamphacks/core/apps/api/internal/config" - "github.com/swamphacks/core/apps/api/internal/database/sqlc" - "github.com/swamphacks/core/apps/api/internal/domains/application" - "github.com/swamphacks/core/apps/api/internal/domains/bat" - "github.com/swamphacks/core/apps/api/internal/domains/hackathon" - "github.com/swamphacks/core/apps/api/internal/tasks" -) - -var ( - ErrEventAlreadyStarted = errors.New("the event has already started") -) - -// BAT Worker -// The BAT worker runs the background execution pipeline for our -// Balanced Admissions Thresher (BAT). This worker processes -// applicant-selection tasks using a combination of review data, -// randomized selection mechanisms, team-formation logic, and -// other decision heuristics. It operates asynchronously to ensure -// fair, consistent, and scalable admissions handling. -type BATWorker struct { - batService *bat.BatService - applicationService *application.ApplicationService - hackathonService *hackathon.HackathonService - scheduler *asynq.Scheduler - taskQueue *asynq.Client - config *config.Config - logger zerolog.Logger -} - -func NewBATWorker( - batService *bat.BatService, applicationService *application.ApplicationService, - hackathonService *hackathon.HackathonService, scheduler *asynq.Scheduler, - taskQueue *asynq.Client, config *config.Config, logger zerolog.Logger, -) *BATWorker { - return &BATWorker{ - batService: batService, - applicationService: applicationService, - hackathonService: hackathonService, - logger: logger.With().Str("worker", "BATWorker").Logger(), - scheduler: scheduler, - config: config, - taskQueue: taskQueue, - } -} - -func (w *BATWorker) HandleCalculateAdmissionsTask(ctx context.Context, t *asynq.Task) error { - var p tasks.CalculateAdmissionsPayload - if err := json.Unmarshal(t.Payload(), &p); err != nil { - w.logger.Err(err).Msg("Failed to unmarshal payload.") - return err - } - - err := w.batService.CalculateAdmissions(ctx, p.BatRunID) - - // On error, mark run as failed. - // To be fair, this should be handled more granulary. Like for example, - // what if the original run was never created? Just food for thought for now. - if err != nil { - w.logger.Err(err).Msg("Something went wrong calculating admissions.") - _, _ = w.batService.UpdateRunById(ctx, sqlc.UpdateBatRunByIdParams{ - ID: p.BatRunID, - StatusDoUpdate: true, - Status: sqlc.BatRunStatusFailed, - }) - - return err - } - - return nil -} -func (w *BATWorker) HandleScheduleTransitionWaitlistTask(ctx context.Context, t *asynq.Task) error { - var payload tasks.ScheduleTransitionWaitlistPayload - if err := json.Unmarshal(t.Payload(), &payload); err != nil { - w.logger.Err(err).Msg("Failed to unmarshal payload.") - return err - } - - hackathon, err := w.hackathonService.GetHackathon(ctx) - if err != nil { - w.logger.Err(err).Msg("Failed to get hackathon in HandleScheduleTransitionWaitlistTask") - return err - } - currentTime := time.Now() - if currentTime.After(hackathon.StartTime) { - w.logger.Err(ErrEventAlreadyStarted).Msg("Could not transition waitlisted applications: the event has already started.") - return ErrEventAlreadyStarted - } - - task, err := tasks.NewTaskTransitionWaitlist(tasks.TransitionWaitlistPayload{ - AcceptFromWaitlistCount: w.config.AcceptFromWaitlistCount, - MaxAcceptedApplications: w.config.MaxAcceptedApplications, - }) - - // The scheduler will make its first run after the period cycles once. So we queue our task immediately as well. - _, err = w.taskQueue.Enqueue(task, asynq.Queue("bat")) - - w.scheduler.Start() - _, err = w.scheduler.Register(payload.Period, task, asynq.Queue("bat")) - if err != nil { - w.logger.Err(err) - return nil - } - - return nil -} - -func (w *BATWorker) HandleTransitionWaitlistTask(ctx context.Context, t *asynq.Task) error { - var payload tasks.TransitionWaitlistPayload - if err := json.Unmarshal(t.Payload(), &payload); err != nil { - w.logger.Err(err).Msg("Failed to unmarshal payload.") - return err - } - - err := w.applicationService.TransitionWaitlistedApplications(ctx, payload.AcceptFromWaitlistCount, payload.MaxAcceptedApplications) - if err != nil { - w.logger.Err(err) - return nil - } - - return nil -} - -func (w *BATWorker) HandleShutdownScheduler(ctx context.Context, t *asynq.Task) error { - w.scheduler.Shutdown() - // Error returned by logging. - - return nil -} +// import ( +// "context" +// "encoding/json" +// "errors" +// "time" + +// "github.com/hibiken/asynq" +// "github.com/rs/zerolog" +// "github.com/swamphacks/core/apps/api/internal/config" +// "github.com/swamphacks/core/apps/api/internal/database/sqlc" +// "github.com/swamphacks/core/apps/api/internal/domains/application" +// "github.com/swamphacks/core/apps/api/internal/domains/bat" +// "github.com/swamphacks/core/apps/api/internal/domains/hackathon" +// "github.com/swamphacks/core/apps/api/internal/tasks" +// ) + +// var ( +// ErrEventAlreadyStarted = errors.New("the event has already started") +// ) + +// // BAT Worker +// // The BAT worker runs the background execution pipeline for our +// // Balanced Admissions Thresher (BAT). This worker processes +// // applicant-selection tasks using a combination of review data, +// // randomized selection mechanisms, team-formation logic, and +// // other decision heuristics. It operates asynchronously to ensure +// // fair, consistent, and scalable admissions handling. +// type BATWorker struct { +// batService *bat.BatService +// applicationService *application.ApplicationService +// hackathonService *hackathon.HackathonService +// scheduler *asynq.Scheduler +// taskQueue *asynq.Client +// config *config.Config +// logger zerolog.Logger +// } + +// func NewBATWorker( +// batService *bat.BatService, applicationService *application.ApplicationService, +// hackathonService *hackathon.HackathonService, scheduler *asynq.Scheduler, +// taskQueue *asynq.Client, config *config.Config, logger zerolog.Logger, +// ) *BATWorker { +// return &BATWorker{ +// batService: batService, +// applicationService: applicationService, +// hackathonService: hackathonService, +// logger: logger.With().Str("worker", "BATWorker").Logger(), +// scheduler: scheduler, +// config: config, +// taskQueue: taskQueue, +// } +// } + +// func (w *BATWorker) HandleCalculateAdmissionsTask(ctx context.Context, t *asynq.Task) error { +// var p tasks.CalculateAdmissionsPayload +// if err := json.Unmarshal(t.Payload(), &p); err != nil { +// w.logger.Err(err).Msg("Failed to unmarshal payload.") +// return err +// } + +// err := w.batService.CalculateAdmissions(ctx, p.BatRunID) + +// // On error, mark run as failed. +// // To be fair, this should be handled more granulary. Like for example, +// // what if the original run was never created? Just food for thought for now. +// if err != nil { +// w.logger.Err(err).Msg("Something went wrong calculating admissions.") +// _, _ = w.batService.UpdateRunById(ctx, sqlc.UpdateBatRunByIdParams{ +// ID: p.BatRunID, +// StatusDoUpdate: true, +// Status: sqlc.BatRunStatusFailed, +// }) + +// return err +// } + +// return nil +// } +// func (w *BATWorker) HandleScheduleTransitionWaitlistTask(ctx context.Context, t *asynq.Task) error { +// var payload tasks.ScheduleTransitionWaitlistPayload +// if err := json.Unmarshal(t.Payload(), &payload); err != nil { +// w.logger.Err(err).Msg("Failed to unmarshal payload.") +// return err +// } + +// hackathon, err := w.hackathonService.GetHackathon(ctx) +// if err != nil { +// w.logger.Err(err).Msg("Failed to get hackathon in HandleScheduleTransitionWaitlistTask") +// return err +// } +// currentTime := time.Now() +// if currentTime.After(hackathon.StartTime) { +// w.logger.Err(ErrEventAlreadyStarted).Msg("Could not transition waitlisted applications: the event has already started.") +// return ErrEventAlreadyStarted +// } + +// task, err := tasks.NewTaskTransitionWaitlist(tasks.TransitionWaitlistPayload{ +// AcceptFromWaitlistCount: w.config.AcceptFromWaitlistCount, +// MaxAcceptedApplications: w.config.MaxAcceptedApplications, +// }) + +// // The scheduler will make its first run after the period cycles once. So we queue our task immediately as well. +// _, err = w.taskQueue.Enqueue(task, asynq.Queue("bat")) + +// w.scheduler.Start() +// _, err = w.scheduler.Register(payload.Period, task, asynq.Queue("bat")) +// if err != nil { +// w.logger.Err(err) +// return nil +// } + +// return nil +// } + +// func (w *BATWorker) HandleTransitionWaitlistTask(ctx context.Context, t *asynq.Task) error { +// var payload tasks.TransitionWaitlistPayload +// if err := json.Unmarshal(t.Payload(), &payload); err != nil { +// w.logger.Err(err).Msg("Failed to unmarshal payload.") +// return err +// } + +// err := w.applicationService.TransitionWaitlistedApplications(ctx, payload.AcceptFromWaitlistCount, payload.MaxAcceptedApplications) +// if err != nil { +// w.logger.Err(err) +// return nil +// } + +// return nil +// } + +// func (w *BATWorker) HandleShutdownScheduler(ctx context.Context, t *asynq.Task) error { +// w.scheduler.Shutdown() +// // Error returned by logging. + +// return nil +// } diff --git a/apps/web/.gitignore b/apps/web/.gitignore index 12f184a1..c2e780af 100644 --- a/apps/web/.gitignore +++ b/apps/web/.gitignore @@ -27,3 +27,5 @@ dist-ssr routeTree.gen.ts *storybook.log .tanstack + +schema.d.ts \ No newline at end of file diff --git a/apps/web/Dockerfile b/apps/web/Dockerfile index 97c325fb..6aaf5a96 100644 --- a/apps/web/Dockerfile +++ b/apps/web/Dockerfile @@ -1,13 +1,14 @@ # ========== Base Stage =========== -FROM node:22.16.0-slim AS base +FROM node:24.16-slim AS base WORKDIR /app # Install pnpm globally RUN npm install -g pnpm -COPY package.json pnpm-lock.yaml ./ +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ +ENV HUSKY=0 # Install dependencies RUN pnpm install --frozen-lockfile @@ -27,7 +28,7 @@ FROM base AS build RUN pnpm run build # ========== Production Stage (With Runtime Config Injection) =========== -FROM node:22.16.0-slim AS prod +FROM node:24.16-slim AS prod WORKDIR /app # Copy built React app (dist folder) diff --git a/apps/web/package.json b/apps/web/package.json index 7043385c..6c4008bd 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -26,9 +26,9 @@ ] }, "dependencies": { - "@internationalized/date": "^3.8.2", - "@react-aria/form": "^3.1.0", - "@react-stately/form": "^3.2.0", + "@internationalized/date": "^3.12.2", + "@react-aria/form": "^3.2.1", + "@react-stately/form": "^3.3.1", "@smastrom/react-rating": "^1.5.0", "@tailwindcss/vite": "^4.1.5", "@tanstack/react-form": "^1.14.1", @@ -46,8 +46,8 @@ "lodash.debounce": "^4.0.8", "nanoid": "^5.1.5", "react": "^19.1.0", - "react-aria": "^3.41.1", - "react-aria-components": "^1.10.1", + "react-aria": "^3.49.0", + "react-aria-components": "^1.18.0", "react-dom": "^19.1.0", "react-error-boundary": "^6.0.0", "react-pdf": "^10.2.0", diff --git a/apps/web/pnpm-lock.yaml b/apps/web/pnpm-lock.yaml index 30430856..5ec14c9d 100644 --- a/apps/web/pnpm-lock.yaml +++ b/apps/web/pnpm-lock.yaml @@ -9,20 +9,20 @@ importers: .: dependencies: '@internationalized/date': - specifier: ^3.8.2 - version: 3.8.2 + specifier: ^3.12.2 + version: 3.12.2 '@react-aria/form': - specifier: ^3.1.0 - version: 3.1.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + specifier: ^3.2.1 + version: 3.2.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@react-stately/form': - specifier: ^3.2.0 - version: 3.2.0(react@19.1.0) + specifier: ^3.3.1 + version: 3.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@smastrom/react-rating': specifier: ^1.5.0 version: 1.5.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@tailwindcss/vite': specifier: ^4.1.5 - version: 4.1.11(vite@6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.20.3)(yaml@2.8.0)) + version: 4.1.11(vite@6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.48.0)(tsx@4.20.3)(yaml@2.8.0)) '@tanstack/react-form': specifier: ^1.14.1 version: 1.14.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -69,11 +69,11 @@ importers: specifier: ^19.1.0 version: 19.1.0 react-aria: - specifier: ^3.41.1 - version: 3.41.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + specifier: ^3.49.0 + version: 3.49.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) react-aria-components: - specifier: ^1.10.1 - version: 1.10.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + specifier: ^1.18.0 + version: 1.18.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) react-dom: specifier: ^19.1.0 version: 19.1.0(react@19.1.0) @@ -91,7 +91,7 @@ importers: version: 5.10.2(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) react-stately: specifier: ^3.39.0 - version: 3.39.0(react@19.1.0) + version: 3.39.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) react-toastify: specifier: ^11.0.5 version: 11.0.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -109,7 +109,7 @@ importers: version: 2.0.0(tailwindcss@4.1.11) vite-plugin-svgr: specifier: ^4.5.0 - version: 4.5.0(rollup@4.44.1)(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.20.3)(yaml@2.8.0)) + version: 4.5.0(rollup@4.44.1)(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.48.0)(tsx@4.20.3)(yaml@2.8.0)) zod: specifier: ^4.0.5 version: 4.0.5 @@ -134,7 +134,7 @@ importers: version: 8.6.14(storybook@8.6.14(prettier@3.5.3)) '@storybook/addon-styling-webpack': specifier: ^1.0.1 - version: 1.0.1(storybook@8.6.14(prettier@3.5.3))(webpack@5.99.9(esbuild@0.25.5)) + version: 1.0.1(storybook@8.6.14(prettier@3.5.3))(webpack@5.99.9(esbuild@0.25.5)(lightningcss@1.30.1)) '@storybook/blocks': specifier: ^8.6.12 version: 8.6.14(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(storybook@8.6.14(prettier@3.5.3)) @@ -146,7 +146,7 @@ importers: version: 8.6.14(@storybook/test@8.6.14(storybook@8.6.14(prettier@3.5.3)))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(storybook@8.6.14(prettier@3.5.3))(typescript@5.8.3) '@storybook/react-vite': specifier: ^8.6.12 - version: 8.6.14(@storybook/test@8.6.14(storybook@8.6.14(prettier@3.5.3)))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(rollup@4.44.1)(storybook@8.6.14(prettier@3.5.3))(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.20.3)(yaml@2.8.0)) + version: 8.6.14(@storybook/test@8.6.14(storybook@8.6.14(prettier@3.5.3)))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(rollup@4.44.1)(storybook@8.6.14(prettier@3.5.3))(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.48.0)(tsx@4.20.3)(yaml@2.8.0)) '@storybook/test': specifier: ^8.6.12 version: 8.6.14(storybook@8.6.14(prettier@3.5.3)) @@ -161,7 +161,7 @@ importers: version: 8.1.0(@svgr/core@8.1.0(typescript@5.8.3)) '@tanstack/router-plugin': specifier: ^1.120.2 - version: 1.123.2(@tanstack/react-router@1.131.47(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(vite@6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.20.3)(yaml@2.8.0))(webpack@5.99.9(esbuild@0.25.5)) + version: 1.123.2(@tanstack/react-router@1.131.47(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(vite@6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.48.0)(tsx@4.20.3)(yaml@2.8.0))(webpack@5.99.9(esbuild@0.25.5)(lightningcss@1.30.1)) '@testing-library/jest-dom': specifier: ^6.6.3 version: 6.6.3 @@ -185,10 +185,10 @@ importers: version: 19.1.6(@types/react@19.1.8) '@vitejs/plugin-react': specifier: ^4.4.1 - version: 4.6.0(vite@6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.20.3)(yaml@2.8.0)) + version: 4.6.0(vite@6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.48.0)(tsx@4.20.3)(yaml@2.8.0)) '@vitest/browser': specifier: ^3.1.3 - version: 3.2.4(playwright@1.53.2)(vite@6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.20.3)(yaml@2.8.0))(vitest@3.2.4) + version: 3.2.4(playwright@1.53.2)(vite@6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.48.0)(tsx@4.20.3)(yaml@2.8.0))(vitest@3.2.4) '@vitest/coverage-v8': specifier: ^3.1.3 version: 3.2.4(@vitest/browser@3.2.4)(vitest@3.2.4) @@ -245,10 +245,10 @@ importers: version: 22.1.0(@svgr/core@8.1.0(typescript@5.8.3)) vite: specifier: ^6.3.5 - version: 6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.20.3)(yaml@2.8.0) + version: 6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.48.0)(tsx@4.20.3)(yaml@2.8.0) vitest: specifier: ^3.1.3 - version: 3.2.4(@types/node@22.15.34)(@vitest/browser@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.20.3)(yaml@2.8.0) + version: 3.2.4(@types/node@22.15.34)(@vitest/browser@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.48.0)(tsx@4.20.3)(yaml@2.8.0) packages: @@ -689,21 +689,6 @@ packages: '@floating-ui/utils@0.2.10': resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} - '@formatjs/ecma402-abstract@2.3.4': - resolution: {integrity: sha512-qrycXDeaORzIqNhBOx0btnhpD1c+/qFIHAN9znofuMJX6QBwtbrmlpWfD4oiUUD2vJUOIYFA/gYtg2KAMGG7sA==} - - '@formatjs/fast-memoize@2.2.7': - resolution: {integrity: sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==} - - '@formatjs/icu-messageformat-parser@2.11.2': - resolution: {integrity: sha512-AfiMi5NOSo2TQImsYAg8UYddsNJ/vUEv/HaNqiFjnI3ZFfWihUtD5QtuX6kHl8+H+d3qvnE/3HZrfzgdWpsLNA==} - - '@formatjs/icu-skeleton-parser@1.8.14': - resolution: {integrity: sha512-i4q4V4qslThK4Ig8SxyD76cp3+QJ3sAqr7f6q9VVfeGtxG9OhiAk3y9XF6Q41OymsKzsGQ6OQQoJNY4/lI8TcQ==} - - '@formatjs/intl-localematcher@0.6.1': - resolution: {integrity: sha512-ePEgLgVCqi2BBFnTMWPfIghu6FkbZnnBVhO2sSxvLfrdFw7wCHAHiDoM2h4NRgjbaY7+B7HgOLZGkK187pZTZg==} - '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -736,17 +721,14 @@ packages: '@iconify/utils@2.3.0': resolution: {integrity: sha512-GmQ78prtwYW6EtzXRU1rY+KwOKfz32PD7iJh6Iyqw68GiKuoZ2A6pRtzWONz5VQJbp50mEjXh/7NkumtrAgRKA==} - '@internationalized/date@3.8.2': - resolution: {integrity: sha512-/wENk7CbvLbkUvX1tu0mwq49CVkkWpkXubGel6birjRPyo6uQ4nQpnq5xZu823zRCwwn82zgHrvgF1vZyvmVgA==} + '@internationalized/date@3.12.2': + resolution: {integrity: sha512-FY1Y+H64NDs+HAF6omlnWxm3mEpfgaCSWtL5l551ZZfImA+kGjPFgrnJrGjH6lfmLL0g8Z/mBu1R3kufeCp6Jw==} - '@internationalized/message@3.1.8': - resolution: {integrity: sha512-Rwk3j/TlYZhn3HQ6PyXUV0XP9Uv42jqZGNegt0BXlxjE6G3+LwHjbQZAGHhCnCPdaA6Tvd3ma/7QzLlLkJxAWA==} + '@internationalized/number@3.6.7': + resolution: {integrity: sha512-3ji1fcrT+FPAK86UqEhB/psHixYo6niWPJtt7+qRaYFynt/BaJG8GhAPimtWUpEiVSTq8ZM8L5psMxGquiB/Vg==} - '@internationalized/number@3.6.3': - resolution: {integrity: sha512-p+Zh1sb6EfrfVaS86jlHGQ9HA66fJhV9x5LiE5vCbZtXEHAuhcmUZUdZ4WrFpUBfNalr2OkAJI5AcKEQF+Lebw==} - - '@internationalized/string@3.2.7': - resolution: {integrity: sha512-D4OHBjrinH+PFZPvfCXvG28n2LSykWcJ7GIioQL+ok0LON15SdfoUssoHzzOUmVZLbRoREsQXVzA6r8JKsbP6A==} + '@internationalized/string@3.2.9': + resolution: {integrity: sha512-kzP/M/mbQxODlmOt4bIQZ2SBVUWUSqMLXooXixnX7noche8WHaQcA+nwFN1K2KCF/cp+LDUhcJsCicwkvhD1pg==} '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} @@ -829,30 +811,35 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@napi-rs/canvas-linux-arm64-musl@0.1.82': resolution: {integrity: sha512-moZWuqepAwWBffdF4JDadt8TgBD02iMhG6I1FHZf8xO20AsIp9rB+p0B8Zma2h2vAF/YMjeFCDmW5un6+zZz9g==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@napi-rs/canvas-linux-riscv64-gnu@0.1.82': resolution: {integrity: sha512-w9++2df2kG9eC9LWYIHIlMLuhIrKGQYfUxs97CwgxYjITeFakIRazI9LYWgVzEc98QZ9x9GQvlicFsrROV59MQ==} engines: {node: '>= 10'} cpu: [riscv64] os: [linux] + libc: [glibc] '@napi-rs/canvas-linux-x64-gnu@0.1.82': resolution: {integrity: sha512-lZulOPwrRi6hEg/17CaqdwWEUfOlIJuhXxincx1aVzsVOCmyHf+xFq4i6liJl1P+x2v6Iz2Z/H5zHvXJCC7Bwg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@napi-rs/canvas-linux-x64-musl@0.1.82': resolution: {integrity: sha512-Be9Wf5RTv1w6GXlTph55K3PH3vsAh1Ax4T1FQY1UYM0QfD0yrwGdnJ8/fhqw7dEgMjd59zIbjJQC8C3msbGn5g==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@napi-rs/canvas-win32-x64-msvc@0.1.82': resolution: {integrity: sha512-LN/i8VrvxTDmEEK1c10z2cdOTkWT76LlTGtyZe5Kr1sqoSomKeExAjbilnu1+oee5lZUgS5yfZ2LNlVhCeARuw==} @@ -883,320 +870,12 @@ packages: '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} - '@react-aria/autocomplete@3.0.0-beta.5': - resolution: {integrity: sha512-zYiVeKGYHStpBXS0mf51k14xkVunU/dFqxumfYXDiiyknxIDE4L1kN7XKo16nus3TkTmJtqBHJrWmzCfNkRd9g==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/breadcrumbs@3.5.26': - resolution: {integrity: sha512-jybk2jy3m9KNmTpzJu87C0nkcMcGbZIyotgK1s8st8aUE2aJlxPZrvGuJTO8GUFZn9TKnCg3JjBC8qS9sizKQg==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/button@3.13.3': - resolution: {integrity: sha512-Xn7eTssaefNPUydogI1qDf7qQWPmb+hGoS1QiCNBodPlRpVDXxlZSIhOqQFnLWHv5+z5UL+vu+joqlSPYHqOFw==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/calendar@3.8.3': - resolution: {integrity: sha512-1TAZADcWbfznXzo4oJEqFgX4IE1chZjWsTSJDWr03UEx3XqIJI8GXm+ylOQUiN4j8xqZ7tl4yNuuslKkzoSjMQ==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/checkbox@3.15.7': - resolution: {integrity: sha512-L64van+K2ZEmCpx/KeZGHoxdxQvVHgfusFRFYZbh3e7YEtDcShvUrTDVKmZkINqnmuhGTDolFDQq+E8fWEpcRg==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/collections@3.0.0-rc.3': - resolution: {integrity: sha512-TX6aAzK/FMTvT78LNdSSacKYDnfBWyW5WzxfoQiu/K/kbZVrYSrQaXFrGjkwGEhgmU0O1S4mupANMEgmgI3wlQ==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/color@3.0.9': - resolution: {integrity: sha512-dWyK8a3kNii8Yuj1/CQivnVVxsgkV8em+sb0oA29w04t+CFRQywpE2OVV3wZTDzOIVaz3pXx7/X012WoF6d/eQ==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/combobox@3.12.5': - resolution: {integrity: sha512-mg9RrOTjxQFPy0BQrlqdp5uUC2pLevIqhZit6OfndmOr7khQ32qepDjXoSwYeeSag/jrokc2cGfXfzOwrgAFaQ==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/datepicker@3.14.5': - resolution: {integrity: sha512-TeV/yXEOQ2QOYMxvetWcWUcZN83evmnmG/uSruTdk93e2nZzs227Gg/M95tzgCYRRACCzSzrGujJhNs12Nh7mg==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/dialog@3.5.27': - resolution: {integrity: sha512-Sp8LWQQYNxkLk2+L0bdWmAd9fz1YIrzvxbHXmAn9Tn6+/4SPnQhkOo+qQwtHFbjqe9fyS7cJZxegXd1RegIFew==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/disclosure@3.0.6': - resolution: {integrity: sha512-swO7U2G1Qhelj08RUiPQ8OEwDWDGj7DgWBmMyU2HjVEihR9wlvwsJTvzmxNQvJJT0l1bxQ/tM4RWxdUycUYy7A==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/dnd@3.10.1': - resolution: {integrity: sha512-EWiFbRoWs0zBlBbdPvd7gPyA3B8TPUtMfSUnLBCjwc+N0YaUoizZxW2VYgpAkZYAlVrPYV6n2Gs+98PHKZ8rsg==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/focus@3.20.5': - resolution: {integrity: sha512-JpFtXmWQ0Oca7FcvkqgjSyo6xEP7v3oQOLUId6o0xTvm4AD5W0mU2r3lYrbhsJ+XxdUUX4AVR5473sZZ85kU4A==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/form@3.1.0': - resolution: {integrity: sha512-aDAOZafrn0V8e09mDAtCvc+JnpnkFM9X8cbI5+fdXsXAA+JxO+3uRRfnJHBlIL0iLc4C4OVWxBxWToV95pg1KA==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/grid@3.14.2': - resolution: {integrity: sha512-5oS6sLq0DishBvPVsWnxGcUdBRXyFXCj8/n02yJvjbID5Mpjn9JIHUSL4ZCZAO7QGCXpvO3PI40vB2F6QUs2VA==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/gridlist@3.13.2': - resolution: {integrity: sha512-mPGhW2+Jke66LJIPrYoAdL5BBiC8iZ9orjoan7TBTCX9Xk87EK1XLm1cTxAylRqGNjnLzy+vp05Zt2fHY4QduA==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/i18n@3.12.10': - resolution: {integrity: sha512-1j00soQ2W0nTgzaaIsGFdMF/5aN60AEdCJPhmXGZiuWdWzMxObN9LQ9vdzYPTjTqyqMdSaSp9DZKs5I26Xovpw==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/interactions@3.25.3': - resolution: {integrity: sha512-J1bhlrNtjPS/fe5uJQ+0c7/jiXniwa4RQlP+Emjfc/iuqpW2RhbF9ou5vROcLzWIyaW8tVMZ468J68rAs/aZ5A==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/interactions@3.25.4': - resolution: {integrity: sha512-HBQMxgUPHrW8V63u9uGgBymkMfj6vdWbB0GgUJY49K9mBKMsypcHeWkWM6+bF7kxRO728/IK8bWDV6whDbqjHg==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/label@3.7.19': - resolution: {integrity: sha512-ZJIj/BKf66q52idy24ErzX77vDGuyQn4neWtu51RRSk4npI3pJqEPsdkPCdo2dlBCo/Uc1pfuLGg2hY3N/ni9Q==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/landmark@3.0.4': - resolution: {integrity: sha512-1U5ce6cqg1qGbK4M4R6vwrhUrKXuUzReZwHaTrXxEY22IMxKDXIZL8G7pFpcKix2XKqjLZWf+g8ngGuNhtQ2QQ==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/link@3.8.3': - resolution: {integrity: sha512-83gS9Bb+FMa4Tae2VQrOxWixqYhqj4MDt4Bn0i3gzsP/sPWr1bwo5DJmXfw16UAXMaccl1rUKSqqHdigqaealw==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/listbox@3.14.6': - resolution: {integrity: sha512-ZaYpBXiS+nUzxAmeCmXyvDcZECuZi1ZLn5y8uJ4ZFRVqSxqplVHodsQKwKqklmAM3+IVDyQx2WB4/HIKTGg2Bw==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/live-announcer@3.4.3': - resolution: {integrity: sha512-nbBmx30tW53Vlbq3BbMxHGbHa7vGE9ItacI+1XAdH2UZDLtdZA5J6U9YC6lokKQCv+aEVO6Zl9YG4yp57YwnGw==} - - '@react-aria/menu@3.18.5': - resolution: {integrity: sha512-mOQb4PcNvDdFhyqF7nxREwc1YUg+pPTiMNcSHlz/MKFkkUteIQBYfuJJa8i72ooiE55xfYEQhPLjmrLHAOIJ+g==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/meter@3.4.24': - resolution: {integrity: sha512-IYI0Z2pwMvIe8r/3G3PHhM4G/KRiW1ssFCBZdCjBbSpl6/EkmrHiyeaBYG0j8Ux8tmRmXiMVjxLdDlCJQDH7mQ==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/numberfield@3.11.16': - resolution: {integrity: sha512-AGk0BMdHXPP3gSy39UVropyvpNMxAElPGIcicjXXyD/tZdemsgLXUFT2zI4DwE0csFZS8BGgunLWT9VluMF4FQ==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/overlays@3.27.3': - resolution: {integrity: sha512-1hawsRI+QiM0TkPNwApNJ2+N49NQTP+48xq0JG8hdEUPChQLDoJ39cvT1sxdg0mnLDzLaAYkZrgfokq9sX6FLA==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/progress@3.4.24': - resolution: {integrity: sha512-lpMVrZlSo1Dulo67COCNrcRkJ+lRrC2PI3iRoOIlqw1Ljz4KFoSGyRudg/MLJ/YrQ+6zmNdz5ytdeThrZwHpPQ==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/radio@3.11.5': - resolution: {integrity: sha512-6BjpeTupQnxetfvC2bqIxWUt6USMqNZoKOoOO7mUL7ESF6/Gp8ocutvQn0VnTxU+7OhdrZX5AACPg/qIQYumVw==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/searchfield@3.8.6': - resolution: {integrity: sha512-fEhNOtOV5yRZ8hkWmFO5Mh8nq63/ePun2dUMLAiW1sCQXTUpN9Oo+T4vsEUabuZ25mHvqgVoCVhAFdMbvZ+W+A==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/select@3.15.7': - resolution: {integrity: sha512-b1PpanLblnXgrvIeYPkL9ELdeE3GQXwoRJLNv9DSKSAyBVx+pm6+4BtzngOBdBidRCcOGEBEYxuUW8hMXjFB8w==} + '@react-aria/form@3.2.1': + resolution: {integrity: sha512-uSNi8/lSFTMPGHzCeAPmFVQ8h8rRulInSVdstRL3hFmyjEOd2gGeKHwssCcLHmujy3T+K/AGeOQiKFpz2hjZEw==} peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - '@react-aria/selection@3.24.3': - resolution: {integrity: sha512-QznlHCUcjFgVALUIVBK4SWJd6osaU9lVaZgU4M8uemoIfOHqnBY3zThkQvEhcw/EJ2RpuYYLPOBYZBnk1knD5A==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/separator@3.4.10': - resolution: {integrity: sha512-T9hJpO6lfg6zHRbs5CZD0eZrWIIjN6LY+EC6X5pQJbJeq6HqviVSQx25q98K430S/EGwHRltY5Bwy+XwlMZfdA==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/slider@3.7.21': - resolution: {integrity: sha512-eWu69KnQ7qCmpYBEkgGLjIuKfFqoHu2W6r9d7ys0ZmX81HPj9DhatGpEgHlnjRfCeSl9wL5h2FY9wnIio82cbg==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/spinbutton@3.6.16': - resolution: {integrity: sha512-Ko1e9GeQiiEXeR3IyPT8STS1Pw4k/1OBs9LqI3WKlHFwH5M8q3DbbaMOgekD41/CPVBKmCcqFM7K7Wu9kFrT2A==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/ssr@3.9.10': - resolution: {integrity: sha512-hvTm77Pf+pMBhuBm760Li0BVIO38jv1IBws1xFm1NoL26PU+fe+FMW5+VZWyANR6nYL65joaJKZqOdTQMkO9IQ==} - engines: {node: '>= 12'} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/ssr@3.9.9': - resolution: {integrity: sha512-2P5thfjfPy/np18e5wD4WPt8ydNXhij1jwA8oehxZTFqlgVMGXzcWKxTb4RtJrLFsqPO7RUQTiY8QJk0M4Vy2g==} - engines: {node: '>= 12'} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/switch@3.7.5': - resolution: {integrity: sha512-GV9rFYf4wRHAh9tkhptvm3uOflKcQHdgZh+eGpSAHyq2iTq0j2nEhlmtFordpcJgC4XWro7TXLNltfqUqVHtkw==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/table@3.17.5': - resolution: {integrity: sha512-Q9HDr2EAhoah7HFIT6XxOOOv2fiAs0agwQQd3d1w6jqgyu9m20lM/jxcSwcCFj2O7FPKHfapSAijHDZZoc4Shg==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/tabs@3.10.5': - resolution: {integrity: sha512-ddmGPikXW+27W2Rx0VuEwwGJVLTo68QkNbSl8R+TEM0EUIAJo3nwHzAlQhuo5Tcb1PdK7biTjO1dyI4pno2/0Q==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/tag@3.6.2': - resolution: {integrity: sha512-xO33FU0bZSpZ3Bw7bnJz7+Me0daVLJrn5dAllf18Mmf9T2cEr63Gg4AL4nR+rj6NLSq0aH8QyDtRGNqXJjo5SQ==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/textfield@3.17.5': - resolution: {integrity: sha512-HFdvqd3Mdp6WP7uYAWD64gRrL1D4Khi+Fm3dIHBhm1ANV0QjYkphJm4DYNDq/MXCZF46+CZNiOWEbL/aeviykA==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/toast@3.0.5': - resolution: {integrity: sha512-uhwiZqPy6hqucBUL7z6uUZjAJ/ou3bNdTjZlXS+zbcm+T0dsjKDfzNkaebyZY7AX3cYkFCaRjc3N6omXwoAviw==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/toggle@3.11.5': - resolution: {integrity: sha512-8+Evk/JVMQ25PNhbnHUvsAK99DAjnCWMdSBNswJ1sWseKCYQzBXsNkkF6Dl/FlSkfDBFAaRHkX9JUz02wehb9A==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/toolbar@3.0.0-beta.18': - resolution: {integrity: sha512-P1fXhmTRBK4YvPQDzCY3XoZl+HiBADgvQ89jszxJ2jD4Qzs/E096ttCc+otZnbvRcoU27IxC2vWFInqK/bP31g==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/tooltip@3.8.5': - resolution: {integrity: sha512-spGAuHHNkiqAfyOl4JWzKEK642KC1oQylioYg+LKCq2avUyaDqFlRx2JrC4a6nt3BV6E5/cJUMV9K7gMRApd5Q==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/tree@3.1.1': - resolution: {integrity: sha512-9LIe9unStA/9HHX6idHdbxMJLjebFP9mngIjoBgbWSNaYx3oH1X3Ei2Q9qHmimebtBagEZgSjxy7M+RcEqFhlw==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/utils@3.29.1': - resolution: {integrity: sha512-yXMFVJ73rbQ/yYE/49n5Uidjw7kh192WNN9PNQGV0Xoc7EJUlSOxqhnpHmYTyO0EotJ8fdM1fMH8durHjUSI8g==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/utils@3.30.0': - resolution: {integrity: sha512-ydA6y5G1+gbem3Va2nczj/0G0W7/jUVo/cbN10WA5IizzWIwMP5qhFr7macgbKfHMkZ+YZC3oXnt2NNre5odKw==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/virtualizer@4.1.7': - resolution: {integrity: sha512-mUJAWuLANVd6mXd7SKbGl9+LqrHxgkH/bo9qQTKaRKDWR3PVqU4m/xdY/u2EDGcWPiiTMHLJaPdMQA5OZ8LtMg==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/visually-hidden@3.8.25': - resolution: {integrity: sha512-9tRRFV1YMLuDId9E8PeUf0xy0KmQBoP8y/bm0PKWzXOqLOVmp/+kop9rwsjC7J6ppbBnlak7XCXTc7GoSFOCRA==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-stately/autocomplete@3.0.0-beta.2': - resolution: {integrity: sha512-6I9vFwRmoxnx5MWA5FCflH6PNjY4+bjE7+sUrFHuDf8BhkwGYtQkRGA45P3KR2gK1dECskG1qqw36lqop4zcaw==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - '@react-stately/calendar@3.8.2': resolution: {integrity: sha512-IGSbTgCMiGYisQ+CwH31wek10UWvNZ1LVwhr0ZNkhDIRtj+p+FuLNtBnmT1CxTFe2Y4empAxyxNA0QSjQrOtvQ==} peerDependencies: @@ -1245,22 +924,17 @@ packages: '@react-stately/flags@3.1.2': resolution: {integrity: sha512-2HjFcZx1MyQXoPqcBGALwWWmgFVUk2TuKVIQxCbRq7fPyWXIl6VHcakCLurdtYC2Iks7zizvz0Idv48MQ38DWg==} - '@react-stately/form@3.2.0': - resolution: {integrity: sha512-PfefxvT7/BIhAGpD4oQpdcxnL8cfN0ZTQxQq+Wmb9z3YzK1oM8GFxb8eGdDRG71JeF8WUNMAQVZFhgl00Z/YKg==} + '@react-stately/form@3.3.1': + resolution: {integrity: sha512-Wz5CK6X4bUo+VBUZLTrRDMXVdlTUVvQbaWTBzINf1zDeiNvGRsnv43L4OSQu/y9xfbtzJz/m2SH2ZTu1//aQXg==} peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 '@react-stately/grid@3.11.3': resolution: {integrity: sha512-/YurYfPARtgsgS5f8rklB7ZQu6MWLdpfTHuwOELEUZ4L52S2gGA5VfLxDnAsHHnu5XHFI3ScuYLAvjWN0rgs/Q==} peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - '@react-stately/layout@4.3.1': - resolution: {integrity: sha512-W2aa60I3qCI24HzZaFsS/eV1aCL0YI3IOlYm9PgsbELP82y3n7YRnwVreUv30KVdpn0VviLZn2xdWSeZlyqi9A==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - '@react-stately/list@3.12.3': resolution: {integrity: sha512-RiqYyxPYAF3YRBEin8/WHC8/hvpZ/fG1Tx3h1W4aXU5zTIBuy0DrjRKePwP90oCiDpztgRXePLlzhgWeKvJEow==} peerDependencies: @@ -1341,32 +1015,6 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - '@react-stately/utils@3.10.8': - resolution: {integrity: sha512-SN3/h7SzRsusVQjQ4v10LaVsDc81jyyR0DD5HnsQitm/I5WDpaSr2nRHtyloPFU48jlql1XX/S04T2DLQM7Y3g==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-stately/virtualizer@4.4.1': - resolution: {integrity: sha512-ZjhsmsNqKY4HrTuT9ySh8lNmYHGgFX24CVVQ3hMr8dTzO9DRR89BMrmenoVtMj7NkonWF8lUFyYlVlsijs2p4w==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-types/autocomplete@3.0.0-alpha.32': - resolution: {integrity: sha512-eRi5n+QMMI3IUMX8z2+dnbQXaTgEgsmp2Qg1a/6HobJzq3IviIjkrG1B4jwp+kZHca7OuVa2ouiWvBu9sW9o4A==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-types/breadcrumbs@3.7.14': - resolution: {integrity: sha512-SbLjrKKupzCLbqHZIQYtQvtsXN53NPxOYyug6QfC4d7DcW1Q9wJ546fxb10Y83ftAJMMUHTatI6SenJVoqyUdA==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-types/button@3.12.2': - resolution: {integrity: sha512-QLoSCX8E7NFIdkVMa65TPieve0rKeltfcIxiMtrphjfNn+83L0IHMcbhjf4r4W19c/zqGbw3E53Hx8mNukoTUw==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - '@react-types/calendar@3.7.2': resolution: {integrity: sha512-Bp6fZo52fZdUjYbtJXcaLQ0jWEOeSoyZVwNyN5G6BmPyLP5nHxMPF+R1MPFR0fdpSI4/Sk78gWzoTuU5eOVQLw==} peerDependencies: @@ -1392,41 +1040,16 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - '@react-types/dialog@3.5.19': - resolution: {integrity: sha512-+FIyFnoKIGNL20zG8Sye7rrRxmt5HoeaCaHhDCTtNtv8CZEhm3Z+kNd4gylgWAxZRhDtBRWko+ADqfN5gQrgKg==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-types/form@3.7.13': - resolution: {integrity: sha512-Ryw9QDLpHi0xsNe+eucgpADeaRSmsd7+SBsL15soEXJ50K/EoPtQOkm6fE4lhfqAX8or12UF9FBcBLULmfCVNQ==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - '@react-types/grid@3.3.3': resolution: {integrity: sha512-VZAKO3XISc/3+a+DZ+hUx2NB/buOe2Ui2nISutv25foeXX4+YpWj5lXS74lJUCuVsSz6D6yoWvEajeUCYrNOxg==} peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - '@react-types/link@3.6.2': - resolution: {integrity: sha512-CtCexoupcaFHJdVPRUpJ83uxK1U0bd9x9DhwRFMqqfPHufICkQkETIw2KIeZXRvMUMi2CSG/81XXy6K0K1MtNw==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-types/listbox@3.7.1': - resolution: {integrity: sha512-WiCihJJpVWVEUxxZjhTbnG3Zq3q38XylKnvNelkVHbF+Y3+SXWN0Yyhk43J642G/d87lw1t60Tor0k96eaz4vw==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - '@react-types/menu@3.10.2': resolution: {integrity: sha512-TVQFGttaNCcIvy1MKavb9ZihJmng46uUtVF9oTG/VI/C4YEdzekteI6iSsXbjv5ZAvOKQR+S25IWCbK2W0YCjQ==} peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - '@react-types/meter@3.4.10': - resolution: {integrity: sha512-soimx+MAngG5MjQplJNB9erPh+P3Er764PqGA75L6FFmf2KhgzMniSVAqyVOpZu7G3qK4O+ihMAYXf6pQMBkSg==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - '@react-types/numberfield@3.8.12': resolution: {integrity: sha512-cI0Grj+iW5840gV80t7aXt7FZPbxMZufjuAop5taHe6RlHuLuODfz5n3kyu/NPHabruF26mVEu0BfIrwZyy+VQ==} peerDependencies: @@ -1437,11 +1060,6 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - '@react-types/progress@3.5.13': - resolution: {integrity: sha512-+4v++AP2xxYxjrTkIXlWWGUhPPIEBzyg76EW0SHKnD4pXxKigcIXEzRbxy62SMidTVdi7jh3tuicIP8OQxJ4cA==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - '@react-types/radio@3.8.10': resolution: {integrity: sha512-hLOu2CXxzxQqkEkXSM71jEJMnU5HvSzwQ+DbJISDjgfgAKvZZHMQX94Fht2Vj+402OdI77esl3pJ1tlSLyV5VQ==} peerDependencies: @@ -1462,18 +1080,18 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - '@react-types/shared@3.31.0': - resolution: {integrity: sha512-ua5U6V66gDcbLZe4P2QeyNgPp4YWD1ymGA6j3n+s8CGExtrCPe64v+g4mvpT8Bnb985R96e4zFT61+m0YCwqMg==} + '@react-types/shared@3.35.0': + resolution: {integrity: sha512-iNWvuzEwANttpQpdlu8nPBtdHb0mcCMj1ZTH//iRB5E/14IAnyRlR25rxH7pNLyzHINsPGEKnWvpwDMCT6vziQ==} peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - '@react-types/slider@3.7.12': - resolution: {integrity: sha512-kOQLrENLpQzmu6TfavdW1yfEc8VPitT4ZNMKOK0h7x3LskEWjptxcZ4IBowEpqHwk0eMbI9lRE/3tsShGUoLwQ==} + '@react-types/shared@3.36.0': + resolution: {integrity: sha512-DkP/H0C2YjjS7gZWKNqOmU8a16qHPjQNdzMwmTq9SzplM6Iw0kVMTZ0OIoe6FOgGqa+FwMsE2QbPjh/n3g/jXQ==} peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - '@react-types/switch@3.5.12': - resolution: {integrity: sha512-6Zz7i+L9k8zw2c3nO8XErxuIy7JVDptz1NTZMiUeyDtLmQnvEKnKPKNjo2j+C/OngtJqAPowC3xRvMXbSAcYqA==} + '@react-types/slider@3.7.12': + resolution: {integrity: sha512-kOQLrENLpQzmu6TfavdW1yfEc8VPitT4ZNMKOK0h7x3LskEWjptxcZ4IBowEpqHwk0eMbI9lRE/3tsShGUoLwQ==} peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 @@ -1553,56 +1171,67 @@ packages: resolution: {integrity: sha512-n0edDmSHlXFhrlmTK7XBuwKlG5MbS7yleS1cQ9nn4kIeW+dJH+ExqNgQ0RrFRew8Y+0V/x6C5IjsHrJmiHtkxQ==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.44.1': resolution: {integrity: sha512-8WVUPy3FtAsKSpyk21kV52HCxB+me6YkbkFHATzC2Yd3yuqHwy2lbFL4alJOLXKljoRw08Zk8/xEj89cLQ/4Nw==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.44.1': resolution: {integrity: sha512-yuktAOaeOgorWDeFJggjuCkMGeITfqvPgkIXhDqsfKX8J3jGyxdDZgBV/2kj/2DyPaLiX6bPdjJDTu9RB8lUPQ==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.44.1': resolution: {integrity: sha512-W+GBM4ifET1Plw8pdVaecwUgxmiH23CfAUj32u8knq0JPFyK4weRy6H7ooxYFD19YxBulL0Ktsflg5XS7+7u9g==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loongarch64-gnu@4.44.1': resolution: {integrity: sha512-1zqnUEMWp9WrGVuVak6jWTl4fEtrVKfZY7CvcBmUUpxAJ7WcSowPSAWIKa/0o5mBL/Ij50SIf9tuirGx63Ovew==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-powerpc64le-gnu@4.44.1': resolution: {integrity: sha512-Rl3JKaRu0LHIx7ExBAAnf0JcOQetQffaw34T8vLlg9b1IhzcBgaIdnvEbbsZq9uZp3uAH+JkHd20Nwn0h9zPjA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.44.1': resolution: {integrity: sha512-j5akelU3snyL6K3N/iX7otLBIl347fGwmd95U5gS/7z6T4ftK288jKq3A5lcFKcx7wwzb5rgNvAg3ZbV4BqUSw==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.44.1': resolution: {integrity: sha512-ppn5llVGgrZw7yxbIm8TTvtj1EoPgYUAbfw0uDjIOzzoqlZlZrLJ/KuiE7uf5EpTpCTrNt1EdtzF0naMm0wGYg==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.44.1': resolution: {integrity: sha512-Hu6hEdix0oxtUma99jSP7xbvjkUM/ycke/AQQ4EC5g7jNRLLIwjcNwaUy95ZKBJJwg1ZowsclNnjYqzN4zwkAw==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.44.1': resolution: {integrity: sha512-EtnsrmZGomz9WxK1bR5079zee3+7a+AdFlghyd6VbAjgRJDbTANJ9dcPIPAi76uG05micpEL+gPGmAKYTschQw==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.44.1': resolution: {integrity: sha512-iAS4p+J1az6Usn0f8xhgL4PaU878KEtutP4hqw52I4IO6AGoyOkHCxcc4bqufv1tQLdDWFx8lR9YlwxKuv3/3g==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-win32-arm64-msvc@4.44.1': resolution: {integrity: sha512-NtSJVKcXwcqozOl+FwI41OH3OApDyLk3kqTJgx8+gp6On9ZEt5mYhIsKNPGuaZr3p9T6NWPKGU/03Vw4CNU9qg==} @@ -1882,8 +1511,8 @@ packages: peerDependencies: '@svgr/core': '*' - '@swc/helpers@0.5.17': - resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==} + '@swc/helpers@0.5.23': + resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==} '@tailwindcss/node@4.1.11': resolution: {integrity: sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q==} @@ -1923,24 +1552,28 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.1.11': resolution: {integrity: sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.1.11': resolution: {integrity: sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.1.11': resolution: {integrity: sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.1.11': resolution: {integrity: sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g==} @@ -2161,6 +1794,9 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + '@types/js-cookie@3.0.6': resolution: {integrity: sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==} @@ -2179,8 +1815,8 @@ packages: '@types/node@22.15.34': resolution: {integrity: sha512-8Y6E5WUupYy1Dd0II32BsWAx5MWdcnRd8L84Oys3veg1YrYtNtzgO4CFhiBg6MDSjk7Ay36HYOnU7/tuOzIzcw==} - '@types/node@22.19.1': - resolution: {integrity: sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==} + '@types/node@22.19.21': + resolution: {integrity: sha512-VMeFBSCKQKmm2swI2kW51SFusDqekC6q9trBCvJ/JliDchFSuoYYKN7yVNjPthP1HKZcx3U1gI/wTcEBjEFKTA==} '@types/parse-json@4.0.2': resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} @@ -2414,6 +2050,11 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + acorn@8.17.0: + resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==} + engines: {node: '>=0.4.0'} + hasBin: true + agent-base@7.1.3: resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==} engines: {node: '>= 14'} @@ -2434,8 +2075,8 @@ packages: ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} - ajv@8.17.1: - resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} ansi-colors@4.1.3: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} @@ -2476,6 +2117,10 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + aria-hidden@1.2.6: + resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} + engines: {node: '>=10'} + aria-query@5.3.0: resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} @@ -2511,8 +2156,9 @@ packages: barcode-detector@3.0.8: resolution: {integrity: sha512-Z9jzzE8ngEDyN9EU7lWdGgV07mcnEQnrX8W9WecXDqD2v+5CcVjt9+a134a5zb+kICvpsrDx6NYA6ay4LGFs8A==} - baseline-browser-mapping@2.8.31: - resolution: {integrity: sha512-a28v2eWrrRWPpJSzxc+mKwm0ZtVx/G8SepdQZDArnXYU/XS+IF6mp8aB/4E+hH1tyGCoDo3KlUCdlSxGDsRkAw==} + baseline-browser-mapping@2.10.38: + resolution: {integrity: sha512-31/02mVB4yuQU6adKk5SlY6m+mxDwUq5KZkyYgnLrrKl7TEm1+3PyDtDBz2kOv/wxZz41GHsvV1A/u6RmiyBvw==} + engines: {node: '>=6.0.0'} hasBin: true better-opn@3.0.2: @@ -2544,8 +2190,8 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true - browserslist@4.28.0: - resolution: {integrity: sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==} + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -2579,8 +2225,8 @@ packages: caniuse-lite@1.0.30001726: resolution: {integrity: sha512-VQAUIUzBiZ/UnlM28fSp2CRF3ivUn1BWEvxMcVTNwpw91Py1pGbPIyIKtd+tzct9C3ouceCVdGAXxZOpZAsgdw==} - caniuse-lite@1.0.30001757: - resolution: {integrity: sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ==} + caniuse-lite@1.0.30001799: + resolution: {integrity: sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==} chai@5.2.0: resolution: {integrity: sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==} @@ -2733,9 +2379,6 @@ packages: decimal.js@10.5.0: resolution: {integrity: sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==} - decimal.js@10.6.0: - resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} - decode-formdata@0.9.0: resolution: {integrity: sha512-q5uwOjR3Um5YD+ZWPOF/1sGHVW9A5rCrRwITQChRXlmPkxDFBqCm4jNTIVdGHNH9OnR+V9MoZVgRhsFb+ARbUw==} @@ -2798,8 +2441,8 @@ packages: electron-to-chromium@1.5.178: resolution: {integrity: sha512-wObbz/ar3Bc6e4X5vf0iO8xTN8YAjN/tgiAOJLr7yjYFtP9wAjq8Mb5h0yn6kResir+VYx2DXBj9NNobs0ETSA==} - electron-to-chromium@1.5.259: - resolution: {integrity: sha512-I+oLXgpEJzD6Cwuwt1gYjxsDmu/S/Kd41mmLA3O+/uH2pFRO/DvOjUyGozL8j3KeLV6WyZ7ssPwELMsXCcsJAQ==} + electron-to-chromium@1.5.375: + resolution: {integrity: sha512-ZWP5eB4BVPW/ZYo9252hQZHZ5XavtsTgpbhcmMmRwymavC5AsLWQWBPaKMeNd2LW0KGby5HPXvj7+sr4ta5j/Q==} emoji-regex@10.4.0: resolution: {integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==} @@ -2814,8 +2457,8 @@ packages: resolution: {integrity: sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==} engines: {node: '>=10.13.0'} - enhanced-resolve@5.18.3: - resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==} + enhanced-resolve@5.24.0: + resolution: {integrity: sha512-SkE2t82KlkkxQRVMVLAGKxLfORGQfrkx5dkj+vlgXRVNEdPc4eZcR+J/Fvj8C+yKSFH5L0q3NFlyufOVQnCcYQ==} engines: {node: '>=10.13.0'} entities@4.5.0: @@ -2975,8 +2618,8 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - fast-uri@3.1.0: - resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + fast-uri@3.1.2: + resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==} fastq@1.19.1: resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} @@ -3072,6 +2715,7 @@ packages: glob@10.4.5: resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true globals@11.12.0: @@ -3177,9 +2821,6 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - intl-messageformat@10.7.16: - resolution: {integrity: sha512-UmdmHUmp5CIKKjSoE10la5yfU+AYJAaiYLsodbjL4lji83JNvgOQUjGaGhGrpFCb0Uh7sl7qfP1IyILa8Z40ug==} - is-arguments@1.2.0: resolution: {integrity: sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==} engines: {node: '>= 0.4'} @@ -3391,24 +3032,28 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-musl@1.30.1: resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-x64-gnu@1.30.1: resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-musl@1.30.1: resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-win32-arm64-msvc@1.30.1: resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==} @@ -3442,8 +3087,8 @@ packages: resolution: {integrity: sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ==} engines: {node: '>=18.0.0'} - loader-runner@4.3.1: - resolution: {integrity: sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==} + loader-runner@4.3.2: + resolution: {integrity: sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==} engines: {node: '>=6.11.5'} local-pkg@1.1.1: @@ -3618,8 +3263,9 @@ packages: node-releases@2.0.19: resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} - node-releases@2.0.27: - resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + node-releases@2.0.48: + resolution: {integrity: sha512-1uz8041X6LoI6ZSdZacM9lVY28vuzDlSKitnpbSNK0RfKoIJkX29NBPVEFXhnuSuEOA9Ww0xnPJ+ILWbGAv8DA==} + engines: {node: '>=18'} normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} @@ -3806,17 +3452,14 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - randombytes@2.1.0: - resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} - - react-aria-components@1.10.1: - resolution: {integrity: sha512-Mllbk2pQax2EwlOJsXG4oTp6P7P33m82/47M9Os+zaGhSCqo2EilFvThxCFxhLa7ncjLV0ka6wFIYLmZiOcWxw==} + react-aria-components@1.18.0: + resolution: {integrity: sha512-FhRQjuDkH4WhgFv+O2sYTzK3JzdZTGpBeaqfRlfTo+DcSZzD8elJEkytHe7SDpcexVKeire8NVd7OruZHfCVoA==} peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-aria@3.41.1: - resolution: {integrity: sha512-5mujwnW6/NHvONDecb7DiWkzI27dzBO1auKt4KkgNuW+Awud1LCaK/NOlHp4xZl3fSfh1ROpdAKERHCh7nvAAQ==} + react-aria@3.49.0: + resolution: {integrity: sha512-4+oK9FwJQWYhyA5zLfj/feOGY0zZbkE1muoF4gyxMroHVypjcYaRSTlJwvxph2zIlxt757KX6xIK2wJ5Aw1Kog==} peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 @@ -3882,6 +3525,16 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-stately@3.47.0: + resolution: {integrity: sha512-H3ar+SOWP920EbVg7qWfP3fZjZiwhlEJAEJQqjt+w8oKijCwFgr0+R4941PIHscOXRNRvEOjvWilitImC0DdBg==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + react-stately@3.48.0: + resolution: {integrity: sha512-ImicSAG+lTotAe5izcs1fz49Zk48w7pDusqYg04WaPhCoej8BJ24soMu3iLXIrsi273s4P1gZrYGrqReMfgEEA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-toastify@11.0.5: resolution: {integrity: sha512-EpqHBGvnSTtHYhCPLxML05NLY2ZX0JURbAdNYa6BUkk+amz4wbKBQvoKQAB0ardvSarUBuY4Q4s1sluAzZwkmA==} peerDependencies: @@ -3948,9 +3601,6 @@ packages: run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - safe-buffer@5.2.1: - resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - safe-regex-test@1.1.0: resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} engines: {node: '>= 0.4'} @@ -3981,9 +3631,6 @@ packages: engines: {node: '>=10'} hasBin: true - serialize-javascript@6.0.2: - resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} - seroval-plugins@1.3.3: resolution: {integrity: sha512-16OL3NnUBw8JG1jBLUoZJsLnQq0n5Ua6aHalhJK4fMQkz1lqR7Osz1sA30trBtd9VUDc2NgkuRCn8+/pBwqZ+w==} engines: {node: '>=10'} @@ -4170,32 +3817,60 @@ packages: resolution: {integrity: sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==} engines: {node: '>=6'} - tapable@2.3.0: - resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} engines: {node: '>=6'} tar@7.4.3: resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} engines: {node: '>=18'} + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me - terser-webpack-plugin@5.3.14: - resolution: {integrity: sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==} + terser-webpack-plugin@5.6.1: + resolution: {integrity: sha512-201R5j+sJpK8nFWwKVyNfZot8FaJbLZDq5evriVzbV1wDtSXDjRUDRfJzHpAaxFDMEhsZL1QkeqM61wgsS3KaQ==} engines: {node: '>= 10.13.0'} peerDependencies: + '@minify-html/node': '*' '@swc/core': '*' + '@swc/css': '*' + '@swc/html': '*' + clean-css: '*' + cssnano: '*' + csso: '*' esbuild: '*' + html-minifier-terser: '*' + lightningcss: '*' + postcss: '*' uglify-js: '*' webpack: ^5.1.0 peerDependenciesMeta: + '@minify-html/node': + optional: true '@swc/core': optional: true + '@swc/css': + optional: true + '@swc/html': + optional: true + clean-css: + optional: true + cssnano: + optional: true + csso: + optional: true esbuild: optional: true + html-minifier-terser: + optional: true + lightningcss: + optional: true + postcss: + optional: true uglify-js: optional: true - terser@5.44.1: - resolution: {integrity: sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==} + terser@5.48.0: + resolution: {integrity: sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q==} engines: {node: '>=10'} hasBin: true @@ -4368,8 +4043,8 @@ packages: peerDependencies: browserslist: '>= 4.21.0' - update-browserslist-db@1.1.4: - resolution: {integrity: sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==} + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' @@ -4394,11 +4069,17 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + util@0.12.5: resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} uuid@9.0.1: resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true uzip@0.20201231.0: @@ -4489,16 +4170,16 @@ packages: warning@4.0.3: resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==} - watchpack@2.4.4: - resolution: {integrity: sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==} + watchpack@2.5.2: + resolution: {integrity: sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==} engines: {node: '>=10.13.0'} webidl-conversions@7.0.0: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} engines: {node: '>=12'} - webpack-sources@3.3.3: - resolution: {integrity: sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==} + webpack-sources@3.5.0: + resolution: {integrity: sha512-HPuy+uuoTCaaoEoI1LQ3JN9+vrPBvEesnnX1jADHy728cHSMlq4wUc4afYqahq2B1mhQVZxCXOkNTnXltr+2vQ==} engines: {node: '>=10.13.0'} webpack-virtual-modules@0.6.2: @@ -4521,6 +4202,7 @@ packages: whatwg-encoding@3.1.1: resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} engines: {node: '>=18'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation whatwg-mimetype@4.0.0: resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} @@ -5073,32 +4755,6 @@ snapshots: '@floating-ui/utils@0.2.10': {} - '@formatjs/ecma402-abstract@2.3.4': - dependencies: - '@formatjs/fast-memoize': 2.2.7 - '@formatjs/intl-localematcher': 0.6.1 - decimal.js: 10.6.0 - tslib: 2.8.1 - - '@formatjs/fast-memoize@2.2.7': - dependencies: - tslib: 2.8.1 - - '@formatjs/icu-messageformat-parser@2.11.2': - dependencies: - '@formatjs/ecma402-abstract': 2.3.4 - '@formatjs/icu-skeleton-parser': 1.8.14 - tslib: 2.8.1 - - '@formatjs/icu-skeleton-parser@1.8.14': - dependencies: - '@formatjs/ecma402-abstract': 2.3.4 - tslib: 2.8.1 - - '@formatjs/intl-localematcher@0.6.1': - dependencies: - tslib: 2.8.1 - '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.6': @@ -5135,22 +4791,17 @@ snapshots: transitivePeerDependencies: - supports-color - '@internationalized/date@3.8.2': - dependencies: - '@swc/helpers': 0.5.17 - - '@internationalized/message@3.1.8': + '@internationalized/date@3.12.2': dependencies: - '@swc/helpers': 0.5.17 - intl-messageformat: 10.7.16 + '@swc/helpers': 0.5.23 - '@internationalized/number@3.6.3': + '@internationalized/number@3.6.7': dependencies: - '@swc/helpers': 0.5.17 + '@swc/helpers': 0.5.23 - '@internationalized/string@3.2.7': + '@internationalized/string@3.2.9': dependencies: - '@swc/helpers': 0.5.17 + '@swc/helpers': 0.5.23 '@isaacs/cliui@8.0.2': dependencies: @@ -5167,12 +4818,12 @@ snapshots: '@istanbuljs/schema@0.1.3': {} - '@joshwooding/vite-plugin-react-docgen-typescript@0.5.0(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.20.3)(yaml@2.8.0))': + '@joshwooding/vite-plugin-react-docgen-typescript@0.5.0(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.48.0)(tsx@4.20.3)(yaml@2.8.0))': dependencies: glob: 10.4.5 magic-string: 0.27.0 react-docgen-typescript: 2.4.0(typescript@5.8.3) - vite: 6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.20.3)(yaml@2.8.0) + vite: 6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.48.0)(tsx@4.20.3)(yaml@2.8.0) optionalDependencies: typescript: 5.8.3 @@ -5193,843 +4844,194 @@ snapshots: '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 - '@jridgewell/sourcemap-codec@1.5.3': {} - - '@jridgewell/sourcemap-codec@1.5.5': {} - - '@jridgewell/trace-mapping@0.3.28': - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.3 - - '@jridgewell/trace-mapping@0.3.31': - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 - - '@mdx-js/react@3.1.0(@types/react@19.1.8)(react@19.1.0)': - dependencies: - '@types/mdx': 2.0.13 - '@types/react': 19.1.8 - react: 19.1.0 - - '@napi-rs/canvas-android-arm64@0.1.82': - optional: true - - '@napi-rs/canvas-darwin-arm64@0.1.82': - optional: true - - '@napi-rs/canvas-darwin-x64@0.1.82': - optional: true - - '@napi-rs/canvas-linux-arm-gnueabihf@0.1.82': - optional: true - - '@napi-rs/canvas-linux-arm64-gnu@0.1.82': - optional: true - - '@napi-rs/canvas-linux-arm64-musl@0.1.82': - optional: true - - '@napi-rs/canvas-linux-riscv64-gnu@0.1.82': - optional: true - - '@napi-rs/canvas-linux-x64-gnu@0.1.82': - optional: true - - '@napi-rs/canvas-linux-x64-musl@0.1.82': - optional: true - - '@napi-rs/canvas-win32-x64-msvc@0.1.82': - optional: true - - '@napi-rs/canvas@0.1.82': - optionalDependencies: - '@napi-rs/canvas-android-arm64': 0.1.82 - '@napi-rs/canvas-darwin-arm64': 0.1.82 - '@napi-rs/canvas-darwin-x64': 0.1.82 - '@napi-rs/canvas-linux-arm-gnueabihf': 0.1.82 - '@napi-rs/canvas-linux-arm64-gnu': 0.1.82 - '@napi-rs/canvas-linux-arm64-musl': 0.1.82 - '@napi-rs/canvas-linux-riscv64-gnu': 0.1.82 - '@napi-rs/canvas-linux-x64-gnu': 0.1.82 - '@napi-rs/canvas-linux-x64-musl': 0.1.82 - '@napi-rs/canvas-win32-x64-msvc': 0.1.82 - optional: true - - '@nodelib/fs.scandir@2.1.5': - dependencies: - '@nodelib/fs.stat': 2.0.5 - run-parallel: 1.2.0 - - '@nodelib/fs.stat@2.0.5': {} - - '@nodelib/fs.walk@1.2.8': - dependencies: - '@nodelib/fs.scandir': 2.1.5 - fastq: 1.19.1 - - '@pkgjs/parseargs@0.11.0': - optional: true - - '@polka/url@1.0.0-next.29': {} - - '@react-aria/autocomplete@3.0.0-beta.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - '@react-aria/combobox': 3.12.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/focus': 3.20.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/i18n': 3.12.10(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/interactions': 3.25.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/listbox': 3.14.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/searchfield': 3.8.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/textfield': 3.17.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/utils': 3.29.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-stately/autocomplete': 3.0.0-beta.2(react@19.1.0) - '@react-stately/combobox': 3.10.6(react@19.1.0) - '@react-types/autocomplete': 3.0.0-alpha.32(react@19.1.0) - '@react-types/button': 3.12.2(react@19.1.0) - '@react-types/shared': 3.30.0(react@19.1.0) - '@swc/helpers': 0.5.17 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - - '@react-aria/breadcrumbs@3.5.26(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - '@react-aria/i18n': 3.12.10(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/link': 3.8.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/utils': 3.29.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-types/breadcrumbs': 3.7.14(react@19.1.0) - '@react-types/shared': 3.30.0(react@19.1.0) - '@swc/helpers': 0.5.17 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - - '@react-aria/button@3.13.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - '@react-aria/interactions': 3.25.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/toolbar': 3.0.0-beta.18(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/utils': 3.29.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-stately/toggle': 3.8.5(react@19.1.0) - '@react-types/button': 3.12.2(react@19.1.0) - '@react-types/shared': 3.30.0(react@19.1.0) - '@swc/helpers': 0.5.17 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - - '@react-aria/calendar@3.8.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - '@internationalized/date': 3.8.2 - '@react-aria/i18n': 3.12.10(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/interactions': 3.25.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/live-announcer': 3.4.3 - '@react-aria/utils': 3.29.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-stately/calendar': 3.8.2(react@19.1.0) - '@react-types/button': 3.12.2(react@19.1.0) - '@react-types/calendar': 3.7.2(react@19.1.0) - '@react-types/shared': 3.30.0(react@19.1.0) - '@swc/helpers': 0.5.17 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - - '@react-aria/checkbox@3.15.7(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - '@react-aria/form': 3.1.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/interactions': 3.25.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/label': 3.7.19(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/toggle': 3.11.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/utils': 3.29.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-stately/checkbox': 3.6.15(react@19.1.0) - '@react-stately/form': 3.2.0(react@19.1.0) - '@react-stately/toggle': 3.8.5(react@19.1.0) - '@react-types/checkbox': 3.9.5(react@19.1.0) - '@react-types/shared': 3.30.0(react@19.1.0) - '@swc/helpers': 0.5.17 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - - '@react-aria/collections@3.0.0-rc.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - '@react-aria/interactions': 3.25.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/ssr': 3.9.9(react@19.1.0) - '@react-aria/utils': 3.29.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-types/shared': 3.30.0(react@19.1.0) - '@swc/helpers': 0.5.17 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - use-sync-external-store: 1.5.0(react@19.1.0) - - '@react-aria/color@3.0.9(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - '@react-aria/i18n': 3.12.10(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/interactions': 3.25.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/numberfield': 3.11.16(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/slider': 3.7.21(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/spinbutton': 3.6.16(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/textfield': 3.17.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/utils': 3.29.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/visually-hidden': 3.8.25(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-stately/color': 3.8.6(react@19.1.0) - '@react-stately/form': 3.2.0(react@19.1.0) - '@react-types/color': 3.0.6(react@19.1.0) - '@react-types/shared': 3.30.0(react@19.1.0) - '@swc/helpers': 0.5.17 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - - '@react-aria/combobox@3.12.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - '@react-aria/focus': 3.20.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/i18n': 3.12.10(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/listbox': 3.14.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/live-announcer': 3.4.3 - '@react-aria/menu': 3.18.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/overlays': 3.27.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/selection': 3.24.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/textfield': 3.17.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/utils': 3.29.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-stately/collections': 3.12.5(react@19.1.0) - '@react-stately/combobox': 3.10.6(react@19.1.0) - '@react-stately/form': 3.2.0(react@19.1.0) - '@react-types/button': 3.12.2(react@19.1.0) - '@react-types/combobox': 3.13.6(react@19.1.0) - '@react-types/shared': 3.30.0(react@19.1.0) - '@swc/helpers': 0.5.17 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - - '@react-aria/datepicker@3.14.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - '@internationalized/date': 3.8.2 - '@internationalized/number': 3.6.3 - '@internationalized/string': 3.2.7 - '@react-aria/focus': 3.20.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/form': 3.1.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/i18n': 3.12.10(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/interactions': 3.25.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/label': 3.7.19(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/spinbutton': 3.6.16(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/utils': 3.29.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-stately/datepicker': 3.14.2(react@19.1.0) - '@react-stately/form': 3.2.0(react@19.1.0) - '@react-types/button': 3.12.2(react@19.1.0) - '@react-types/calendar': 3.7.2(react@19.1.0) - '@react-types/datepicker': 3.12.2(react@19.1.0) - '@react-types/dialog': 3.5.19(react@19.1.0) - '@react-types/shared': 3.30.0(react@19.1.0) - '@swc/helpers': 0.5.17 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - - '@react-aria/dialog@3.5.27(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - '@react-aria/interactions': 3.25.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/overlays': 3.27.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/utils': 3.29.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-types/dialog': 3.5.19(react@19.1.0) - '@react-types/shared': 3.30.0(react@19.1.0) - '@swc/helpers': 0.5.17 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - - '@react-aria/disclosure@3.0.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - '@react-aria/ssr': 3.9.9(react@19.1.0) - '@react-aria/utils': 3.29.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-stately/disclosure': 3.0.5(react@19.1.0) - '@react-types/button': 3.12.2(react@19.1.0) - '@swc/helpers': 0.5.17 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - - '@react-aria/dnd@3.10.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - '@internationalized/string': 3.2.7 - '@react-aria/i18n': 3.12.10(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/interactions': 3.25.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/live-announcer': 3.4.3 - '@react-aria/overlays': 3.27.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/utils': 3.29.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-stately/collections': 3.12.5(react@19.1.0) - '@react-stately/dnd': 3.6.0(react@19.1.0) - '@react-types/button': 3.12.2(react@19.1.0) - '@react-types/shared': 3.30.0(react@19.1.0) - '@swc/helpers': 0.5.17 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - - '@react-aria/focus@3.20.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - '@react-aria/interactions': 3.25.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/utils': 3.29.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-types/shared': 3.30.0(react@19.1.0) - '@swc/helpers': 0.5.17 - clsx: 2.1.1 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - - '@react-aria/form@3.1.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - '@react-aria/interactions': 3.25.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/utils': 3.30.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-stately/form': 3.2.0(react@19.1.0) - '@react-types/shared': 3.31.0(react@19.1.0) - '@swc/helpers': 0.5.17 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - - '@react-aria/grid@3.14.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - '@react-aria/focus': 3.20.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/i18n': 3.12.10(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/interactions': 3.25.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/live-announcer': 3.4.3 - '@react-aria/selection': 3.24.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/utils': 3.29.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-stately/collections': 3.12.5(react@19.1.0) - '@react-stately/grid': 3.11.3(react@19.1.0) - '@react-stately/selection': 3.20.3(react@19.1.0) - '@react-types/checkbox': 3.9.5(react@19.1.0) - '@react-types/grid': 3.3.3(react@19.1.0) - '@react-types/shared': 3.30.0(react@19.1.0) - '@swc/helpers': 0.5.17 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - - '@react-aria/gridlist@3.13.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - '@react-aria/focus': 3.20.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/grid': 3.14.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/i18n': 3.12.10(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/interactions': 3.25.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/selection': 3.24.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/utils': 3.29.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-stately/collections': 3.12.5(react@19.1.0) - '@react-stately/list': 3.12.3(react@19.1.0) - '@react-stately/tree': 3.9.0(react@19.1.0) - '@react-types/shared': 3.30.0(react@19.1.0) - '@swc/helpers': 0.5.17 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - - '@react-aria/i18n@3.12.10(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - '@internationalized/date': 3.8.2 - '@internationalized/message': 3.1.8 - '@internationalized/number': 3.6.3 - '@internationalized/string': 3.2.7 - '@react-aria/ssr': 3.9.9(react@19.1.0) - '@react-aria/utils': 3.29.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-types/shared': 3.30.0(react@19.1.0) - '@swc/helpers': 0.5.17 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - - '@react-aria/interactions@3.25.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - '@react-aria/ssr': 3.9.9(react@19.1.0) - '@react-aria/utils': 3.29.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-stately/flags': 3.1.2 - '@react-types/shared': 3.30.0(react@19.1.0) - '@swc/helpers': 0.5.17 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - - '@react-aria/interactions@3.25.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - '@react-aria/ssr': 3.9.10(react@19.1.0) - '@react-aria/utils': 3.30.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-stately/flags': 3.1.2 - '@react-types/shared': 3.31.0(react@19.1.0) - '@swc/helpers': 0.5.17 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - - '@react-aria/label@3.7.19(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - '@react-aria/utils': 3.29.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-types/shared': 3.30.0(react@19.1.0) - '@swc/helpers': 0.5.17 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - - '@react-aria/landmark@3.0.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - '@react-aria/utils': 3.29.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-types/shared': 3.30.0(react@19.1.0) - '@swc/helpers': 0.5.17 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - use-sync-external-store: 1.5.0(react@19.1.0) - - '@react-aria/link@3.8.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - '@react-aria/interactions': 3.25.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/utils': 3.29.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-types/link': 3.6.2(react@19.1.0) - '@react-types/shared': 3.30.0(react@19.1.0) - '@swc/helpers': 0.5.17 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - - '@react-aria/listbox@3.14.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - '@react-aria/interactions': 3.25.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/label': 3.7.19(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/selection': 3.24.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/utils': 3.29.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-stately/collections': 3.12.5(react@19.1.0) - '@react-stately/list': 3.12.3(react@19.1.0) - '@react-types/listbox': 3.7.1(react@19.1.0) - '@react-types/shared': 3.30.0(react@19.1.0) - '@swc/helpers': 0.5.17 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - - '@react-aria/live-announcer@3.4.3': - dependencies: - '@swc/helpers': 0.5.17 - - '@react-aria/menu@3.18.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - '@react-aria/focus': 3.20.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/i18n': 3.12.10(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/interactions': 3.25.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/overlays': 3.27.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/selection': 3.24.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/utils': 3.29.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-stately/collections': 3.12.5(react@19.1.0) - '@react-stately/menu': 3.9.5(react@19.1.0) - '@react-stately/selection': 3.20.3(react@19.1.0) - '@react-stately/tree': 3.9.0(react@19.1.0) - '@react-types/button': 3.12.2(react@19.1.0) - '@react-types/menu': 3.10.2(react@19.1.0) - '@react-types/shared': 3.30.0(react@19.1.0) - '@swc/helpers': 0.5.17 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - - '@react-aria/meter@3.4.24(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - '@react-aria/progress': 3.4.24(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-types/meter': 3.4.10(react@19.1.0) - '@react-types/shared': 3.30.0(react@19.1.0) - '@swc/helpers': 0.5.17 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - - '@react-aria/numberfield@3.11.16(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - '@react-aria/i18n': 3.12.10(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/interactions': 3.25.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/spinbutton': 3.6.16(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/textfield': 3.17.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/utils': 3.29.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-stately/form': 3.2.0(react@19.1.0) - '@react-stately/numberfield': 3.9.13(react@19.1.0) - '@react-types/button': 3.12.2(react@19.1.0) - '@react-types/numberfield': 3.8.12(react@19.1.0) - '@react-types/shared': 3.30.0(react@19.1.0) - '@swc/helpers': 0.5.17 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - - '@react-aria/overlays@3.27.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - '@react-aria/focus': 3.20.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/i18n': 3.12.10(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/interactions': 3.25.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/ssr': 3.9.9(react@19.1.0) - '@react-aria/utils': 3.29.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/visually-hidden': 3.8.25(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-stately/overlays': 3.6.17(react@19.1.0) - '@react-types/button': 3.12.2(react@19.1.0) - '@react-types/overlays': 3.8.16(react@19.1.0) - '@react-types/shared': 3.30.0(react@19.1.0) - '@swc/helpers': 0.5.17 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - - '@react-aria/progress@3.4.24(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - '@react-aria/i18n': 3.12.10(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/label': 3.7.19(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/utils': 3.29.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-types/progress': 3.5.13(react@19.1.0) - '@react-types/shared': 3.30.0(react@19.1.0) - '@swc/helpers': 0.5.17 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - - '@react-aria/radio@3.11.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - '@react-aria/focus': 3.20.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/form': 3.1.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/i18n': 3.12.10(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/interactions': 3.25.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/label': 3.7.19(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/utils': 3.29.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-stately/radio': 3.10.14(react@19.1.0) - '@react-types/radio': 3.8.10(react@19.1.0) - '@react-types/shared': 3.30.0(react@19.1.0) - '@swc/helpers': 0.5.17 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - - '@react-aria/searchfield@3.8.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - '@react-aria/i18n': 3.12.10(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/textfield': 3.17.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/utils': 3.29.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-stately/searchfield': 3.5.13(react@19.1.0) - '@react-types/button': 3.12.2(react@19.1.0) - '@react-types/searchfield': 3.6.3(react@19.1.0) - '@react-types/shared': 3.30.0(react@19.1.0) - '@swc/helpers': 0.5.17 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - - '@react-aria/select@3.15.7(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - '@react-aria/form': 3.1.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/i18n': 3.12.10(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/interactions': 3.25.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/label': 3.7.19(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/listbox': 3.14.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/menu': 3.18.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/selection': 3.24.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/utils': 3.29.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/visually-hidden': 3.8.25(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-stately/select': 3.6.14(react@19.1.0) - '@react-types/button': 3.12.2(react@19.1.0) - '@react-types/select': 3.9.13(react@19.1.0) - '@react-types/shared': 3.30.0(react@19.1.0) - '@swc/helpers': 0.5.17 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - - '@react-aria/selection@3.24.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - '@react-aria/focus': 3.20.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/i18n': 3.12.10(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/interactions': 3.25.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/utils': 3.29.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-stately/selection': 3.20.3(react@19.1.0) - '@react-types/shared': 3.30.0(react@19.1.0) - '@swc/helpers': 0.5.17 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - - '@react-aria/separator@3.4.10(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - '@react-aria/utils': 3.29.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-types/shared': 3.30.0(react@19.1.0) - '@swc/helpers': 0.5.17 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - - '@react-aria/slider@3.7.21(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - '@react-aria/i18n': 3.12.10(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/interactions': 3.25.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/label': 3.7.19(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/utils': 3.29.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-stately/slider': 3.6.5(react@19.1.0) - '@react-types/shared': 3.30.0(react@19.1.0) - '@react-types/slider': 3.7.12(react@19.1.0) - '@swc/helpers': 0.5.17 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) + '@jridgewell/sourcemap-codec@1.5.3': {} - '@react-aria/spinbutton@3.6.16(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - '@react-aria/i18n': 3.12.10(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/live-announcer': 3.4.3 - '@react-aria/utils': 3.29.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-types/button': 3.12.2(react@19.1.0) - '@react-types/shared': 3.30.0(react@19.1.0) - '@swc/helpers': 0.5.17 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) + '@jridgewell/sourcemap-codec@1.5.5': {} - '@react-aria/ssr@3.9.10(react@19.1.0)': + '@jridgewell/trace-mapping@0.3.28': dependencies: - '@swc/helpers': 0.5.17 - react: 19.1.0 + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.3 - '@react-aria/ssr@3.9.9(react@19.1.0)': + '@jridgewell/trace-mapping@0.3.31': dependencies: - '@swc/helpers': 0.5.17 - react: 19.1.0 + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 - '@react-aria/switch@3.7.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@mdx-js/react@3.1.0(@types/react@19.1.8)(react@19.1.0)': dependencies: - '@react-aria/toggle': 3.11.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-stately/toggle': 3.8.5(react@19.1.0) - '@react-types/shared': 3.30.0(react@19.1.0) - '@react-types/switch': 3.5.12(react@19.1.0) - '@swc/helpers': 0.5.17 + '@types/mdx': 2.0.13 + '@types/react': 19.1.8 react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - '@react-aria/table@3.17.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - '@react-aria/focus': 3.20.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/grid': 3.14.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/i18n': 3.12.10(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/interactions': 3.25.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/live-announcer': 3.4.3 - '@react-aria/utils': 3.29.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/visually-hidden': 3.8.25(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-stately/collections': 3.12.5(react@19.1.0) - '@react-stately/flags': 3.1.2 - '@react-stately/table': 3.14.3(react@19.1.0) - '@react-types/checkbox': 3.9.5(react@19.1.0) - '@react-types/grid': 3.3.3(react@19.1.0) - '@react-types/shared': 3.30.0(react@19.1.0) - '@react-types/table': 3.13.1(react@19.1.0) - '@swc/helpers': 0.5.17 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) + '@napi-rs/canvas-android-arm64@0.1.82': + optional: true - '@react-aria/tabs@3.10.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - '@react-aria/focus': 3.20.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/i18n': 3.12.10(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/selection': 3.24.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/utils': 3.29.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-stately/tabs': 3.8.3(react@19.1.0) - '@react-types/shared': 3.30.0(react@19.1.0) - '@react-types/tabs': 3.3.16(react@19.1.0) - '@swc/helpers': 0.5.17 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) + '@napi-rs/canvas-darwin-arm64@0.1.82': + optional: true - '@react-aria/tag@3.6.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - '@react-aria/gridlist': 3.13.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/i18n': 3.12.10(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/interactions': 3.25.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/label': 3.7.19(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/selection': 3.24.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/utils': 3.29.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-stately/list': 3.12.3(react@19.1.0) - '@react-types/button': 3.12.2(react@19.1.0) - '@react-types/shared': 3.30.0(react@19.1.0) - '@swc/helpers': 0.5.17 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) + '@napi-rs/canvas-darwin-x64@0.1.82': + optional: true - '@react-aria/textfield@3.17.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - '@react-aria/form': 3.1.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/interactions': 3.25.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/label': 3.7.19(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/utils': 3.29.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-stately/form': 3.2.0(react@19.1.0) - '@react-stately/utils': 3.10.7(react@19.1.0) - '@react-types/shared': 3.30.0(react@19.1.0) - '@react-types/textfield': 3.12.3(react@19.1.0) - '@swc/helpers': 0.5.17 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) + '@napi-rs/canvas-linux-arm-gnueabihf@0.1.82': + optional: true - '@react-aria/toast@3.0.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - '@react-aria/i18n': 3.12.10(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/interactions': 3.25.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/landmark': 3.0.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/utils': 3.29.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-stately/toast': 3.1.1(react@19.1.0) - '@react-types/button': 3.12.2(react@19.1.0) - '@react-types/shared': 3.30.0(react@19.1.0) - '@swc/helpers': 0.5.17 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) + '@napi-rs/canvas-linux-arm64-gnu@0.1.82': + optional: true - '@react-aria/toggle@3.11.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - '@react-aria/interactions': 3.25.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/utils': 3.29.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-stately/toggle': 3.8.5(react@19.1.0) - '@react-types/checkbox': 3.9.5(react@19.1.0) - '@react-types/shared': 3.30.0(react@19.1.0) - '@swc/helpers': 0.5.17 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) + '@napi-rs/canvas-linux-arm64-musl@0.1.82': + optional: true - '@react-aria/toolbar@3.0.0-beta.18(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - '@react-aria/focus': 3.20.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/i18n': 3.12.10(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/utils': 3.29.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-types/shared': 3.30.0(react@19.1.0) - '@swc/helpers': 0.5.17 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) + '@napi-rs/canvas-linux-riscv64-gnu@0.1.82': + optional: true - '@react-aria/tooltip@3.8.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - '@react-aria/interactions': 3.25.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/utils': 3.29.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-stately/tooltip': 3.5.5(react@19.1.0) - '@react-types/shared': 3.30.0(react@19.1.0) - '@react-types/tooltip': 3.4.18(react@19.1.0) - '@swc/helpers': 0.5.17 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) + '@napi-rs/canvas-linux-x64-gnu@0.1.82': + optional: true - '@react-aria/tree@3.1.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - '@react-aria/gridlist': 3.13.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/i18n': 3.12.10(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/selection': 3.24.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/utils': 3.29.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-stately/tree': 3.9.0(react@19.1.0) - '@react-types/button': 3.12.2(react@19.1.0) - '@react-types/shared': 3.30.0(react@19.1.0) - '@swc/helpers': 0.5.17 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) + '@napi-rs/canvas-linux-x64-musl@0.1.82': + optional: true - '@react-aria/utils@3.29.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - '@react-aria/ssr': 3.9.9(react@19.1.0) - '@react-stately/flags': 3.1.2 - '@react-stately/utils': 3.10.7(react@19.1.0) - '@react-types/shared': 3.30.0(react@19.1.0) - '@swc/helpers': 0.5.17 - clsx: 2.1.1 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) + '@napi-rs/canvas-win32-x64-msvc@0.1.82': + optional: true - '@react-aria/utils@3.30.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - '@react-aria/ssr': 3.9.10(react@19.1.0) - '@react-stately/flags': 3.1.2 - '@react-stately/utils': 3.10.8(react@19.1.0) - '@react-types/shared': 3.31.0(react@19.1.0) - '@swc/helpers': 0.5.17 - clsx: 2.1.1 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) + '@napi-rs/canvas@0.1.82': + optionalDependencies: + '@napi-rs/canvas-android-arm64': 0.1.82 + '@napi-rs/canvas-darwin-arm64': 0.1.82 + '@napi-rs/canvas-darwin-x64': 0.1.82 + '@napi-rs/canvas-linux-arm-gnueabihf': 0.1.82 + '@napi-rs/canvas-linux-arm64-gnu': 0.1.82 + '@napi-rs/canvas-linux-arm64-musl': 0.1.82 + '@napi-rs/canvas-linux-riscv64-gnu': 0.1.82 + '@napi-rs/canvas-linux-x64-gnu': 0.1.82 + '@napi-rs/canvas-linux-x64-musl': 0.1.82 + '@napi-rs/canvas-win32-x64-msvc': 0.1.82 + optional: true - '@react-aria/virtualizer@4.1.7(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@nodelib/fs.scandir@2.1.5': dependencies: - '@react-aria/i18n': 3.12.10(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/interactions': 3.25.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/utils': 3.29.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-stately/virtualizer': 4.4.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-types/shared': 3.30.0(react@19.1.0) - '@swc/helpers': 0.5.17 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 - '@react-aria/visually-hidden@3.8.25(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': dependencies: - '@react-aria/interactions': 3.25.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/utils': 3.29.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-types/shared': 3.30.0(react@19.1.0) - '@swc/helpers': 0.5.17 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.19.1 + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@polka/url@1.0.0-next.29': {} - '@react-stately/autocomplete@3.0.0-beta.2(react@19.1.0)': + '@react-aria/form@3.2.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: - '@react-stately/utils': 3.10.7(react@19.1.0) - '@swc/helpers': 0.5.17 + '@swc/helpers': 0.5.23 react: 19.1.0 + react-aria: 3.49.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react-dom: 19.1.0(react@19.1.0) '@react-stately/calendar@3.8.2(react@19.1.0)': dependencies: - '@internationalized/date': 3.8.2 + '@internationalized/date': 3.12.2 '@react-stately/utils': 3.10.7(react@19.1.0) '@react-types/calendar': 3.7.2(react@19.1.0) '@react-types/shared': 3.30.0(react@19.1.0) - '@swc/helpers': 0.5.17 + '@swc/helpers': 0.5.23 react: 19.1.0 - '@react-stately/checkbox@3.6.15(react@19.1.0)': + '@react-stately/checkbox@3.6.15(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: - '@react-stately/form': 3.2.0(react@19.1.0) + '@react-stately/form': 3.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@react-stately/utils': 3.10.7(react@19.1.0) '@react-types/checkbox': 3.9.5(react@19.1.0) '@react-types/shared': 3.30.0(react@19.1.0) - '@swc/helpers': 0.5.17 + '@swc/helpers': 0.5.23 react: 19.1.0 + transitivePeerDependencies: + - react-dom '@react-stately/collections@3.12.5(react@19.1.0)': dependencies: '@react-types/shared': 3.30.0(react@19.1.0) - '@swc/helpers': 0.5.17 + '@swc/helpers': 0.5.23 react: 19.1.0 - '@react-stately/color@3.8.6(react@19.1.0)': + '@react-stately/color@3.8.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: - '@internationalized/number': 3.6.3 - '@internationalized/string': 3.2.7 - '@react-stately/form': 3.2.0(react@19.1.0) - '@react-stately/numberfield': 3.9.13(react@19.1.0) + '@internationalized/number': 3.6.7 + '@internationalized/string': 3.2.9 + '@react-stately/form': 3.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@react-stately/numberfield': 3.9.13(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@react-stately/slider': 3.6.5(react@19.1.0) '@react-stately/utils': 3.10.7(react@19.1.0) '@react-types/color': 3.0.6(react@19.1.0) '@react-types/shared': 3.30.0(react@19.1.0) - '@swc/helpers': 0.5.17 + '@swc/helpers': 0.5.23 react: 19.1.0 + transitivePeerDependencies: + - react-dom - '@react-stately/combobox@3.10.6(react@19.1.0)': + '@react-stately/combobox@3.10.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@react-stately/collections': 3.12.5(react@19.1.0) - '@react-stately/form': 3.2.0(react@19.1.0) + '@react-stately/form': 3.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@react-stately/list': 3.12.3(react@19.1.0) '@react-stately/overlays': 3.6.17(react@19.1.0) - '@react-stately/select': 3.6.14(react@19.1.0) + '@react-stately/select': 3.6.14(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@react-stately/utils': 3.10.7(react@19.1.0) '@react-types/combobox': 3.13.6(react@19.1.0) '@react-types/shared': 3.30.0(react@19.1.0) - '@swc/helpers': 0.5.17 + '@swc/helpers': 0.5.23 react: 19.1.0 + transitivePeerDependencies: + - react-dom '@react-stately/data@3.13.1(react@19.1.0)': dependencies: '@react-types/shared': 3.30.0(react@19.1.0) - '@swc/helpers': 0.5.17 + '@swc/helpers': 0.5.23 react: 19.1.0 - '@react-stately/datepicker@3.14.2(react@19.1.0)': + '@react-stately/datepicker@3.14.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: - '@internationalized/date': 3.8.2 - '@internationalized/string': 3.2.7 - '@react-stately/form': 3.2.0(react@19.1.0) + '@internationalized/date': 3.12.2 + '@internationalized/string': 3.2.9 + '@react-stately/form': 3.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@react-stately/overlays': 3.6.17(react@19.1.0) '@react-stately/utils': 3.10.7(react@19.1.0) '@react-types/datepicker': 3.12.2(react@19.1.0) '@react-types/shared': 3.30.0(react@19.1.0) - '@swc/helpers': 0.5.17 + '@swc/helpers': 0.5.23 react: 19.1.0 + transitivePeerDependencies: + - react-dom '@react-stately/disclosure@3.0.5(react@19.1.0)': dependencies: '@react-stately/utils': 3.10.7(react@19.1.0) '@react-types/shared': 3.30.0(react@19.1.0) - '@swc/helpers': 0.5.17 + '@swc/helpers': 0.5.23 react: 19.1.0 '@react-stately/dnd@3.6.0(react@19.1.0)': dependencies: '@react-stately/selection': 3.20.3(react@19.1.0) '@react-types/shared': 3.30.0(react@19.1.0) - '@swc/helpers': 0.5.17 + '@swc/helpers': 0.5.23 react: 19.1.0 '@react-stately/flags@3.1.2': dependencies: - '@swc/helpers': 0.5.17 + '@swc/helpers': 0.5.23 - '@react-stately/form@3.2.0(react@19.1.0)': + '@react-stately/form@3.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: - '@react-types/shared': 3.31.0(react@19.1.0) - '@swc/helpers': 0.5.17 + '@swc/helpers': 0.5.23 react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + react-stately: 3.48.0(react@19.1.0) '@react-stately/grid@3.11.3(react@19.1.0)': dependencies: @@ -6037,20 +5039,8 @@ snapshots: '@react-stately/selection': 3.20.3(react@19.1.0) '@react-types/grid': 3.3.3(react@19.1.0) '@react-types/shared': 3.30.0(react@19.1.0) - '@swc/helpers': 0.5.17 - react: 19.1.0 - - '@react-stately/layout@4.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - '@react-stately/collections': 3.12.5(react@19.1.0) - '@react-stately/table': 3.14.3(react@19.1.0) - '@react-stately/virtualizer': 4.4.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-types/grid': 3.3.3(react@19.1.0) - '@react-types/shared': 3.30.0(react@19.1.0) - '@react-types/table': 3.13.1(react@19.1.0) - '@swc/helpers': 0.5.17 + '@swc/helpers': 0.5.23 react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) '@react-stately/list@3.12.3(react@19.1.0)': dependencies: @@ -6058,7 +5048,7 @@ snapshots: '@react-stately/selection': 3.20.3(react@19.1.0) '@react-stately/utils': 3.10.7(react@19.1.0) '@react-types/shared': 3.30.0(react@19.1.0) - '@swc/helpers': 0.5.17 + '@swc/helpers': 0.5.23 react: 19.1.0 '@react-stately/menu@3.9.5(react@19.1.0)': @@ -6066,57 +5056,63 @@ snapshots: '@react-stately/overlays': 3.6.17(react@19.1.0) '@react-types/menu': 3.10.2(react@19.1.0) '@react-types/shared': 3.30.0(react@19.1.0) - '@swc/helpers': 0.5.17 + '@swc/helpers': 0.5.23 react: 19.1.0 - '@react-stately/numberfield@3.9.13(react@19.1.0)': + '@react-stately/numberfield@3.9.13(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: - '@internationalized/number': 3.6.3 - '@react-stately/form': 3.2.0(react@19.1.0) + '@internationalized/number': 3.6.7 + '@react-stately/form': 3.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@react-stately/utils': 3.10.7(react@19.1.0) '@react-types/numberfield': 3.8.12(react@19.1.0) - '@swc/helpers': 0.5.17 + '@swc/helpers': 0.5.23 react: 19.1.0 + transitivePeerDependencies: + - react-dom '@react-stately/overlays@3.6.17(react@19.1.0)': dependencies: '@react-stately/utils': 3.10.7(react@19.1.0) '@react-types/overlays': 3.8.16(react@19.1.0) - '@swc/helpers': 0.5.17 + '@swc/helpers': 0.5.23 react: 19.1.0 - '@react-stately/radio@3.10.14(react@19.1.0)': + '@react-stately/radio@3.10.14(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: - '@react-stately/form': 3.2.0(react@19.1.0) + '@react-stately/form': 3.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@react-stately/utils': 3.10.7(react@19.1.0) '@react-types/radio': 3.8.10(react@19.1.0) '@react-types/shared': 3.30.0(react@19.1.0) - '@swc/helpers': 0.5.17 + '@swc/helpers': 0.5.23 react: 19.1.0 + transitivePeerDependencies: + - react-dom '@react-stately/searchfield@3.5.13(react@19.1.0)': dependencies: '@react-stately/utils': 3.10.7(react@19.1.0) '@react-types/searchfield': 3.6.3(react@19.1.0) - '@swc/helpers': 0.5.17 + '@swc/helpers': 0.5.23 react: 19.1.0 - '@react-stately/select@3.6.14(react@19.1.0)': + '@react-stately/select@3.6.14(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: - '@react-stately/form': 3.2.0(react@19.1.0) + '@react-stately/form': 3.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@react-stately/list': 3.12.3(react@19.1.0) '@react-stately/overlays': 3.6.17(react@19.1.0) '@react-types/select': 3.9.13(react@19.1.0) '@react-types/shared': 3.30.0(react@19.1.0) - '@swc/helpers': 0.5.17 + '@swc/helpers': 0.5.23 react: 19.1.0 + transitivePeerDependencies: + - react-dom '@react-stately/selection@3.20.3(react@19.1.0)': dependencies: '@react-stately/collections': 3.12.5(react@19.1.0) '@react-stately/utils': 3.10.7(react@19.1.0) '@react-types/shared': 3.30.0(react@19.1.0) - '@swc/helpers': 0.5.17 + '@swc/helpers': 0.5.23 react: 19.1.0 '@react-stately/slider@3.6.5(react@19.1.0)': @@ -6124,7 +5120,7 @@ snapshots: '@react-stately/utils': 3.10.7(react@19.1.0) '@react-types/shared': 3.30.0(react@19.1.0) '@react-types/slider': 3.7.12(react@19.1.0) - '@swc/helpers': 0.5.17 + '@swc/helpers': 0.5.23 react: 19.1.0 '@react-stately/table@3.14.3(react@19.1.0)': @@ -6137,7 +5133,7 @@ snapshots: '@react-types/grid': 3.3.3(react@19.1.0) '@react-types/shared': 3.30.0(react@19.1.0) '@react-types/table': 3.13.1(react@19.1.0) - '@swc/helpers': 0.5.17 + '@swc/helpers': 0.5.23 react: 19.1.0 '@react-stately/tabs@3.8.3(react@19.1.0)': @@ -6145,28 +5141,28 @@ snapshots: '@react-stately/list': 3.12.3(react@19.1.0) '@react-types/shared': 3.30.0(react@19.1.0) '@react-types/tabs': 3.3.16(react@19.1.0) - '@swc/helpers': 0.5.17 + '@swc/helpers': 0.5.23 react: 19.1.0 '@react-stately/toast@3.1.1(react@19.1.0)': dependencies: - '@swc/helpers': 0.5.17 + '@swc/helpers': 0.5.23 react: 19.1.0 - use-sync-external-store: 1.5.0(react@19.1.0) + use-sync-external-store: 1.6.0(react@19.1.0) '@react-stately/toggle@3.8.5(react@19.1.0)': dependencies: '@react-stately/utils': 3.10.7(react@19.1.0) '@react-types/checkbox': 3.9.5(react@19.1.0) '@react-types/shared': 3.30.0(react@19.1.0) - '@swc/helpers': 0.5.17 + '@swc/helpers': 0.5.23 react: 19.1.0 '@react-stately/tooltip@3.5.5(react@19.1.0)': dependencies: '@react-stately/overlays': 3.6.17(react@19.1.0) '@react-types/tooltip': 3.4.18(react@19.1.0) - '@swc/helpers': 0.5.17 + '@swc/helpers': 0.5.23 react: 19.1.0 '@react-stately/tree@3.9.0(react@19.1.0)': @@ -6175,48 +5171,17 @@ snapshots: '@react-stately/selection': 3.20.3(react@19.1.0) '@react-stately/utils': 3.10.7(react@19.1.0) '@react-types/shared': 3.30.0(react@19.1.0) - '@swc/helpers': 0.5.17 + '@swc/helpers': 0.5.23 react: 19.1.0 '@react-stately/utils@3.10.7(react@19.1.0)': dependencies: - '@swc/helpers': 0.5.17 - react: 19.1.0 - - '@react-stately/utils@3.10.8(react@19.1.0)': - dependencies: - '@swc/helpers': 0.5.17 - react: 19.1.0 - - '@react-stately/virtualizer@4.4.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - '@react-aria/utils': 3.29.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-types/shared': 3.30.0(react@19.1.0) - '@swc/helpers': 0.5.17 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - - '@react-types/autocomplete@3.0.0-alpha.32(react@19.1.0)': - dependencies: - '@react-types/combobox': 3.13.6(react@19.1.0) - '@react-types/searchfield': 3.6.3(react@19.1.0) - '@react-types/shared': 3.30.0(react@19.1.0) - react: 19.1.0 - - '@react-types/breadcrumbs@3.7.14(react@19.1.0)': - dependencies: - '@react-types/link': 3.6.2(react@19.1.0) - '@react-types/shared': 3.30.0(react@19.1.0) - react: 19.1.0 - - '@react-types/button@3.12.2(react@19.1.0)': - dependencies: - '@react-types/shared': 3.30.0(react@19.1.0) + '@swc/helpers': 0.5.23 react: 19.1.0 '@react-types/calendar@3.7.2(react@19.1.0)': dependencies: - '@internationalized/date': 3.8.2 + '@internationalized/date': 3.12.2 '@react-types/shared': 3.30.0(react@19.1.0) react: 19.1.0 @@ -6238,49 +5203,23 @@ snapshots: '@react-types/datepicker@3.12.2(react@19.1.0)': dependencies: - '@internationalized/date': 3.8.2 + '@internationalized/date': 3.12.2 '@react-types/calendar': 3.7.2(react@19.1.0) '@react-types/overlays': 3.8.16(react@19.1.0) '@react-types/shared': 3.30.0(react@19.1.0) react: 19.1.0 - '@react-types/dialog@3.5.19(react@19.1.0)': - dependencies: - '@react-types/overlays': 3.8.16(react@19.1.0) - '@react-types/shared': 3.30.0(react@19.1.0) - react: 19.1.0 - - '@react-types/form@3.7.13(react@19.1.0)': - dependencies: - '@react-types/shared': 3.30.0(react@19.1.0) - react: 19.1.0 - '@react-types/grid@3.3.3(react@19.1.0)': dependencies: '@react-types/shared': 3.30.0(react@19.1.0) react: 19.1.0 - '@react-types/link@3.6.2(react@19.1.0)': - dependencies: - '@react-types/shared': 3.30.0(react@19.1.0) - react: 19.1.0 - - '@react-types/listbox@3.7.1(react@19.1.0)': - dependencies: - '@react-types/shared': 3.30.0(react@19.1.0) - react: 19.1.0 - '@react-types/menu@3.10.2(react@19.1.0)': dependencies: '@react-types/overlays': 3.8.16(react@19.1.0) '@react-types/shared': 3.30.0(react@19.1.0) react: 19.1.0 - '@react-types/meter@3.4.10(react@19.1.0)': - dependencies: - '@react-types/progress': 3.5.13(react@19.1.0) - react: 19.1.0 - '@react-types/numberfield@3.8.12(react@19.1.0)': dependencies: '@react-types/shared': 3.30.0(react@19.1.0) @@ -6291,11 +5230,6 @@ snapshots: '@react-types/shared': 3.30.0(react@19.1.0) react: 19.1.0 - '@react-types/progress@3.5.13(react@19.1.0)': - dependencies: - '@react-types/shared': 3.30.0(react@19.1.0) - react: 19.1.0 - '@react-types/radio@3.8.10(react@19.1.0)': dependencies: '@react-types/shared': 3.30.0(react@19.1.0) @@ -6316,16 +5250,15 @@ snapshots: dependencies: react: 19.1.0 - '@react-types/shared@3.31.0(react@19.1.0)': + '@react-types/shared@3.35.0(react@19.1.0)': dependencies: react: 19.1.0 - '@react-types/slider@3.7.12(react@19.1.0)': + '@react-types/shared@3.36.0(react@19.1.0)': dependencies: - '@react-types/shared': 3.30.0(react@19.1.0) react: 19.1.0 - '@react-types/switch@3.5.12(react@19.1.0)': + '@react-types/slider@3.7.12(react@19.1.0)': dependencies: '@react-types/shared': 3.30.0(react@19.1.0) react: 19.1.0 @@ -6523,10 +5456,10 @@ snapshots: storybook: 8.6.14(prettier@3.5.3) ts-dedent: 2.2.0 - '@storybook/addon-styling-webpack@1.0.1(storybook@8.6.14(prettier@3.5.3))(webpack@5.99.9(esbuild@0.25.5))': + '@storybook/addon-styling-webpack@1.0.1(storybook@8.6.14(prettier@3.5.3))(webpack@5.99.9(esbuild@0.25.5)(lightningcss@1.30.1))': dependencies: '@storybook/node-logger': 8.6.14(storybook@8.6.14(prettier@3.5.3)) - webpack: 5.99.9(esbuild@0.25.5) + webpack: 5.99.9(esbuild@0.25.5)(lightningcss@1.30.1) transitivePeerDependencies: - storybook @@ -6548,13 +5481,13 @@ snapshots: react: 19.1.0 react-dom: 19.1.0(react@19.1.0) - '@storybook/builder-vite@8.6.14(storybook@8.6.14(prettier@3.5.3))(vite@6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.20.3)(yaml@2.8.0))': + '@storybook/builder-vite@8.6.14(storybook@8.6.14(prettier@3.5.3))(vite@6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.48.0)(tsx@4.20.3)(yaml@2.8.0))': dependencies: '@storybook/csf-plugin': 8.6.14(storybook@8.6.14(prettier@3.5.3)) browser-assert: 1.2.1 storybook: 8.6.14(prettier@3.5.3) ts-dedent: 2.2.0 - vite: 6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.20.3)(yaml@2.8.0) + vite: 6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.48.0)(tsx@4.20.3)(yaml@2.8.0) '@storybook/components@8.6.14(storybook@8.6.14(prettier@3.5.3))': dependencies: @@ -6601,9 +5534,9 @@ snapshots: storybook: 8.6.14(prettier@3.5.3) ts-dedent: 2.2.0 optionalDependencies: - '@vitest/browser': 3.2.4(playwright@1.53.2)(vite@6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.20.3)(yaml@2.8.0))(vitest@3.2.4) + '@vitest/browser': 3.2.4(playwright@1.53.2)(vite@6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.48.0)(tsx@4.20.3)(yaml@2.8.0))(vitest@3.2.4) '@vitest/runner': 3.2.4 - vitest: 3.2.4(@types/node@22.15.34)(@vitest/browser@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.20.3)(yaml@2.8.0) + vitest: 3.2.4(@types/node@22.15.34)(@vitest/browser@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.48.0)(tsx@4.20.3)(yaml@2.8.0) transitivePeerDependencies: - react - react-dom @@ -6639,11 +5572,11 @@ snapshots: react-dom: 19.1.0(react@19.1.0) storybook: 8.6.14(prettier@3.5.3) - '@storybook/react-vite@8.6.14(@storybook/test@8.6.14(storybook@8.6.14(prettier@3.5.3)))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(rollup@4.44.1)(storybook@8.6.14(prettier@3.5.3))(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.20.3)(yaml@2.8.0))': + '@storybook/react-vite@8.6.14(@storybook/test@8.6.14(storybook@8.6.14(prettier@3.5.3)))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(rollup@4.44.1)(storybook@8.6.14(prettier@3.5.3))(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.48.0)(tsx@4.20.3)(yaml@2.8.0))': dependencies: - '@joshwooding/vite-plugin-react-docgen-typescript': 0.5.0(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.20.3)(yaml@2.8.0)) + '@joshwooding/vite-plugin-react-docgen-typescript': 0.5.0(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.48.0)(tsx@4.20.3)(yaml@2.8.0)) '@rollup/pluginutils': 5.2.0(rollup@4.44.1) - '@storybook/builder-vite': 8.6.14(storybook@8.6.14(prettier@3.5.3))(vite@6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.20.3)(yaml@2.8.0)) + '@storybook/builder-vite': 8.6.14(storybook@8.6.14(prettier@3.5.3))(vite@6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.48.0)(tsx@4.20.3)(yaml@2.8.0)) '@storybook/react': 8.6.14(@storybook/test@8.6.14(storybook@8.6.14(prettier@3.5.3)))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(storybook@8.6.14(prettier@3.5.3))(typescript@5.8.3) find-up: 5.0.0 magic-string: 0.30.17 @@ -6653,7 +5586,7 @@ snapshots: resolve: 1.22.10 storybook: 8.6.14(prettier@3.5.3) tsconfig-paths: 4.2.0 - vite: 6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.20.3)(yaml@2.8.0) + vite: 6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.48.0)(tsx@4.20.3)(yaml@2.8.0) optionalDependencies: '@storybook/test': 8.6.14(storybook@8.6.14(prettier@3.5.3)) transitivePeerDependencies: @@ -6761,7 +5694,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@swc/helpers@0.5.17': + '@swc/helpers@0.5.23': dependencies: tslib: 2.8.1 @@ -6829,12 +5762,12 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.1.11 '@tailwindcss/oxide-win32-x64-msvc': 4.1.11 - '@tailwindcss/vite@4.1.11(vite@6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.20.3)(yaml@2.8.0))': + '@tailwindcss/vite@4.1.11(vite@6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.48.0)(tsx@4.20.3)(yaml@2.8.0))': dependencies: '@tailwindcss/node': 4.1.11 '@tailwindcss/oxide': 4.1.11 tailwindcss: 4.1.11 - vite: 6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.20.3)(yaml@2.8.0) + vite: 6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.48.0)(tsx@4.20.3)(yaml@2.8.0) '@tanstack/form-core@1.14.0': dependencies: @@ -6877,7 +5810,7 @@ snapshots: '@tanstack/store': 0.7.1 react: 19.1.0 react-dom: 19.1.0(react@19.1.0) - use-sync-external-store: 1.5.0(react@19.1.0) + use-sync-external-store: 1.6.0(react@19.1.0) '@tanstack/react-store@0.7.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: @@ -6924,7 +5857,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@tanstack/router-plugin@1.123.2(@tanstack/react-router@1.131.47(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(vite@6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.20.3)(yaml@2.8.0))(webpack@5.99.9(esbuild@0.25.5))': + '@tanstack/router-plugin@1.123.2(@tanstack/react-router@1.131.47(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(vite@6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.48.0)(tsx@4.20.3)(yaml@2.8.0))(webpack@5.99.9(esbuild@0.25.5)(lightningcss@1.30.1))': dependencies: '@babel/core': 7.27.7 '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.27.7) @@ -6942,8 +5875,8 @@ snapshots: zod: 3.25.76 optionalDependencies: '@tanstack/react-router': 1.131.47(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - vite: 6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.20.3)(yaml@2.8.0) - webpack: 5.99.9(esbuild@0.25.5) + vite: 6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.48.0)(tsx@4.20.3)(yaml@2.8.0) + webpack: 5.99.9(esbuild@0.25.5)(lightningcss@1.30.1) transitivePeerDependencies: - supports-color @@ -7053,15 +5986,17 @@ snapshots: '@types/eslint-scope@3.7.7': dependencies: '@types/eslint': 9.6.1 - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 '@types/eslint@9.6.1': dependencies: - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 '@types/json-schema': 7.0.15 '@types/estree@1.0.8': {} + '@types/estree@1.0.9': {} + '@types/js-cookie@3.0.6': {} '@types/json-schema@7.0.15': {} @@ -7078,7 +6013,7 @@ snapshots: dependencies: undici-types: 6.21.0 - '@types/node@22.19.1': + '@types/node@22.19.21': dependencies: undici-types: 6.21.0 @@ -7197,7 +6132,7 @@ snapshots: react: 19.1.0 react-dom: 19.1.0(react@19.1.0) - '@vitejs/plugin-react@4.6.0(vite@6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.20.3)(yaml@2.8.0))': + '@vitejs/plugin-react@4.6.0(vite@6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.48.0)(tsx@4.20.3)(yaml@2.8.0))': dependencies: '@babel/core': 7.27.7 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.27.7) @@ -7205,20 +6140,20 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.19 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.20.3)(yaml@2.8.0) + vite: 6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.48.0)(tsx@4.20.3)(yaml@2.8.0) transitivePeerDependencies: - supports-color - '@vitest/browser@3.2.4(playwright@1.53.2)(vite@6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.20.3)(yaml@2.8.0))(vitest@3.2.4)': + '@vitest/browser@3.2.4(playwright@1.53.2)(vite@6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.48.0)(tsx@4.20.3)(yaml@2.8.0))(vitest@3.2.4)': dependencies: '@testing-library/dom': 10.4.0 '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.0) - '@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.20.3)(yaml@2.8.0)) + '@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.48.0)(tsx@4.20.3)(yaml@2.8.0)) '@vitest/utils': 3.2.4 magic-string: 0.30.17 sirv: 3.0.1 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/node@22.15.34)(@vitest/browser@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.20.3)(yaml@2.8.0) + vitest: 3.2.4(@types/node@22.15.34)(@vitest/browser@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.48.0)(tsx@4.20.3)(yaml@2.8.0) ws: 8.18.3 optionalDependencies: playwright: 1.53.2 @@ -7243,9 +6178,9 @@ snapshots: std-env: 3.9.0 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/node@22.15.34)(@vitest/browser@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.20.3)(yaml@2.8.0) + vitest: 3.2.4(@types/node@22.15.34)(@vitest/browser@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.48.0)(tsx@4.20.3)(yaml@2.8.0) optionalDependencies: - '@vitest/browser': 3.2.4(playwright@1.53.2)(vite@6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.20.3)(yaml@2.8.0))(vitest@3.2.4) + '@vitest/browser': 3.2.4(playwright@1.53.2)(vite@6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.48.0)(tsx@4.20.3)(yaml@2.8.0))(vitest@3.2.4) transitivePeerDependencies: - supports-color @@ -7264,13 +6199,13 @@ snapshots: chai: 5.2.0 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(vite@6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.20.3)(yaml@2.8.0))': + '@vitest/mocker@3.2.4(vite@6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.48.0)(tsx@4.20.3)(yaml@2.8.0))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: 6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.20.3)(yaml@2.8.0) + vite: 6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.48.0)(tsx@4.20.3)(yaml@2.8.0) '@vitest/pretty-format@2.0.5': dependencies: @@ -7418,15 +6353,17 @@ snapshots: acorn@8.15.0: {} + acorn@8.17.0: {} + agent-base@7.1.3: {} - ajv-formats@2.1.1(ajv@8.17.1): + ajv-formats@2.1.1(ajv@8.20.0): optionalDependencies: - ajv: 8.17.1 + ajv: 8.20.0 - ajv-keywords@5.1.0(ajv@8.17.1): + ajv-keywords@5.1.0(ajv@8.20.0): dependencies: - ajv: 8.17.1 + ajv: 8.20.0 fast-deep-equal: 3.1.3 ajv@6.12.6: @@ -7436,10 +6373,10 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 - ajv@8.17.1: + ajv@8.20.0: dependencies: fast-deep-equal: 3.1.3 - fast-uri: 3.1.0 + fast-uri: 3.1.2 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 @@ -7470,6 +6407,10 @@ snapshots: argparse@2.0.1: {} + aria-hidden@1.2.6: + dependencies: + tslib: 2.8.1 + aria-query@5.3.0: dependencies: dequal: 2.0.3 @@ -7515,7 +6456,7 @@ snapshots: transitivePeerDependencies: - '@types/emscripten' - baseline-browser-mapping@2.8.31: {} + baseline-browser-mapping@2.10.38: {} better-opn@3.0.2: dependencies: @@ -7549,13 +6490,13 @@ snapshots: node-releases: 2.0.19 update-browserslist-db: 1.1.3(browserslist@4.25.1) - browserslist@4.28.0: + browserslist@4.28.2: dependencies: - baseline-browser-mapping: 2.8.31 - caniuse-lite: 1.0.30001757 - electron-to-chromium: 1.5.259 - node-releases: 2.0.27 - update-browserslist-db: 1.1.4(browserslist@4.28.0) + baseline-browser-mapping: 2.10.38 + caniuse-lite: 1.0.30001799 + electron-to-chromium: 1.5.375 + node-releases: 2.0.48 + update-browserslist-db: 1.2.3(browserslist@4.28.2) buffer-from@1.1.2: {} @@ -7584,7 +6525,7 @@ snapshots: caniuse-lite@1.0.30001726: {} - caniuse-lite@1.0.30001757: {} + caniuse-lite@1.0.30001799: {} chai@5.2.0: dependencies: @@ -7716,8 +6657,6 @@ snapshots: decimal.js@10.5.0: {} - decimal.js@10.6.0: {} - decode-formdata@0.9.0: {} deep-eql@5.0.2: {} @@ -7773,7 +6712,7 @@ snapshots: electron-to-chromium@1.5.178: {} - electron-to-chromium@1.5.259: {} + electron-to-chromium@1.5.375: {} emoji-regex@10.4.0: {} @@ -7786,10 +6725,10 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.2.2 - enhanced-resolve@5.18.3: + enhanced-resolve@5.24.0: dependencies: graceful-fs: 4.2.11 - tapable: 2.3.0 + tapable: 2.3.3 entities@4.5.0: {} @@ -7986,7 +6925,7 @@ snapshots: fast-levenshtein@2.0.6: {} - fast-uri@3.1.0: {} + fast-uri@3.1.2: {} fastq@1.19.1: dependencies: @@ -8163,13 +7102,6 @@ snapshots: inherits@2.0.4: {} - intl-messageformat@10.7.16: - dependencies: - '@formatjs/ecma402-abstract': 2.3.4 - '@formatjs/fast-memoize': 2.2.7 - '@formatjs/icu-messageformat-parser': 2.11.2 - tslib: 2.8.1 - is-arguments@1.2.0: dependencies: call-bound: 1.0.4 @@ -8264,7 +7196,7 @@ snapshots: jest-worker@27.5.1: dependencies: - '@types/node': 22.19.1 + '@types/node': 22.19.21 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -8419,7 +7351,7 @@ snapshots: rfdc: 1.4.1 wrap-ansi: 9.0.0 - loader-runner@4.3.1: {} + loader-runner@4.3.2: {} local-pkg@1.1.1: dependencies: @@ -8568,7 +7500,7 @@ snapshots: node-releases@2.0.19: {} - node-releases@2.0.27: {} + node-releases@2.0.48: {} normalize-path@3.0.0: {} @@ -8742,89 +7674,30 @@ snapshots: queue-microtask@1.2.3: {} - randombytes@2.1.0: - dependencies: - safe-buffer: 5.2.1 - - react-aria-components@1.10.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0): - dependencies: - '@internationalized/date': 3.8.2 - '@internationalized/string': 3.2.7 - '@react-aria/autocomplete': 3.0.0-beta.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/collections': 3.0.0-rc.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/dnd': 3.10.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/focus': 3.20.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/interactions': 3.25.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/live-announcer': 3.4.3 - '@react-aria/overlays': 3.27.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/ssr': 3.9.9(react@19.1.0) - '@react-aria/toolbar': 3.0.0-beta.18(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/utils': 3.29.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/virtualizer': 4.1.7(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-stately/autocomplete': 3.0.0-beta.2(react@19.1.0) - '@react-stately/layout': 4.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-stately/selection': 3.20.3(react@19.1.0) - '@react-stately/table': 3.14.3(react@19.1.0) - '@react-stately/utils': 3.10.7(react@19.1.0) - '@react-stately/virtualizer': 4.4.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-types/form': 3.7.13(react@19.1.0) - '@react-types/grid': 3.3.3(react@19.1.0) - '@react-types/shared': 3.30.0(react@19.1.0) - '@react-types/table': 3.13.1(react@19.1.0) - '@swc/helpers': 0.5.17 + react-aria-components@1.18.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + dependencies: + '@internationalized/date': 3.12.2 + '@react-types/shared': 3.35.0(react@19.1.0) + '@swc/helpers': 0.5.23 client-only: 0.0.1 react: 19.1.0 - react-aria: 3.41.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react-aria: 3.49.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) react-dom: 19.1.0(react@19.1.0) - react-stately: 3.39.0(react@19.1.0) - use-sync-external-store: 1.5.0(react@19.1.0) + react-stately: 3.47.0(react@19.1.0) - react-aria@3.41.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0): - dependencies: - '@internationalized/string': 3.2.7 - '@react-aria/breadcrumbs': 3.5.26(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/button': 3.13.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/calendar': 3.8.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/checkbox': 3.15.7(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/color': 3.0.9(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/combobox': 3.12.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/datepicker': 3.14.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/dialog': 3.5.27(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/disclosure': 3.0.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/dnd': 3.10.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/focus': 3.20.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/gridlist': 3.13.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/i18n': 3.12.10(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/interactions': 3.25.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/label': 3.7.19(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/landmark': 3.0.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/link': 3.8.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/listbox': 3.14.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/menu': 3.18.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/meter': 3.4.24(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/numberfield': 3.11.16(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/overlays': 3.27.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/progress': 3.4.24(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/radio': 3.11.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/searchfield': 3.8.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/select': 3.15.7(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/selection': 3.24.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/separator': 3.4.10(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/slider': 3.7.21(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/ssr': 3.9.9(react@19.1.0) - '@react-aria/switch': 3.7.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/table': 3.17.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/tabs': 3.10.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/tag': 3.6.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/textfield': 3.17.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/toast': 3.0.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/tooltip': 3.8.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/tree': 3.1.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/utils': 3.29.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-aria/visually-hidden': 3.8.25(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-types/shared': 3.30.0(react@19.1.0) + react-aria@3.49.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + dependencies: + '@internationalized/date': 3.12.2 + '@internationalized/number': 3.6.7 + '@internationalized/string': 3.2.9 + '@react-types/shared': 3.35.0(react@19.1.0) + '@swc/helpers': 0.5.23 + aria-hidden: 1.2.6 + clsx: 2.1.1 react: 19.1.0 react-dom: 19.1.0(react@19.1.0) + react-stately: 3.47.0(react@19.1.0) + use-sync-external-store: 1.6.0(react@19.1.0) react-confetti@6.4.0(react@19.1.0): dependencies: @@ -8904,25 +7777,25 @@ snapshots: - '@types/react' - supports-color - react-stately@3.39.0(react@19.1.0): + react-stately@3.39.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: '@react-stately/calendar': 3.8.2(react@19.1.0) - '@react-stately/checkbox': 3.6.15(react@19.1.0) + '@react-stately/checkbox': 3.6.15(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@react-stately/collections': 3.12.5(react@19.1.0) - '@react-stately/color': 3.8.6(react@19.1.0) - '@react-stately/combobox': 3.10.6(react@19.1.0) + '@react-stately/color': 3.8.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@react-stately/combobox': 3.10.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@react-stately/data': 3.13.1(react@19.1.0) - '@react-stately/datepicker': 3.14.2(react@19.1.0) + '@react-stately/datepicker': 3.14.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@react-stately/disclosure': 3.0.5(react@19.1.0) '@react-stately/dnd': 3.6.0(react@19.1.0) - '@react-stately/form': 3.2.0(react@19.1.0) + '@react-stately/form': 3.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@react-stately/list': 3.12.3(react@19.1.0) '@react-stately/menu': 3.9.5(react@19.1.0) - '@react-stately/numberfield': 3.9.13(react@19.1.0) + '@react-stately/numberfield': 3.9.13(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@react-stately/overlays': 3.6.17(react@19.1.0) - '@react-stately/radio': 3.10.14(react@19.1.0) + '@react-stately/radio': 3.10.14(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@react-stately/searchfield': 3.5.13(react@19.1.0) - '@react-stately/select': 3.6.14(react@19.1.0) + '@react-stately/select': 3.6.14(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@react-stately/selection': 3.20.3(react@19.1.0) '@react-stately/slider': 3.6.5(react@19.1.0) '@react-stately/table': 3.14.3(react@19.1.0) @@ -8933,6 +7806,28 @@ snapshots: '@react-stately/tree': 3.9.0(react@19.1.0) '@react-types/shared': 3.30.0(react@19.1.0) react: 19.1.0 + transitivePeerDependencies: + - react-dom + + react-stately@3.47.0(react@19.1.0): + dependencies: + '@internationalized/date': 3.12.2 + '@internationalized/number': 3.6.7 + '@internationalized/string': 3.2.9 + '@react-types/shared': 3.35.0(react@19.1.0) + '@swc/helpers': 0.5.23 + react: 19.1.0 + use-sync-external-store: 1.6.0(react@19.1.0) + + react-stately@3.48.0(react@19.1.0): + dependencies: + '@internationalized/date': 3.12.2 + '@internationalized/number': 3.6.7 + '@internationalized/string': 3.2.9 + '@react-types/shared': 3.36.0(react@19.1.0) + '@swc/helpers': 0.5.23 + react: 19.1.0 + use-sync-external-store: 1.6.0(react@19.1.0) react-toastify@11.0.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: @@ -9021,8 +7916,6 @@ snapshots: dependencies: queue-microtask: 1.2.3 - safe-buffer@5.2.1: {} - safe-regex-test@1.1.0: dependencies: call-bound: 1.0.4 @@ -9040,9 +7933,9 @@ snapshots: schema-utils@4.3.3: dependencies: '@types/json-schema': 7.0.15 - ajv: 8.17.1 - ajv-formats: 2.1.1(ajv@8.17.1) - ajv-keywords: 5.1.0(ajv@8.17.1) + ajv: 8.20.0 + ajv-formats: 2.1.1(ajv@8.20.0) + ajv-keywords: 5.1.0(ajv@8.20.0) sdp@3.2.1: {} @@ -9050,10 +7943,6 @@ snapshots: semver@7.7.2: {} - serialize-javascript@6.0.2: - dependencies: - randombytes: 2.1.0 - seroval-plugins@1.3.3(seroval@1.3.2): dependencies: seroval: 1.3.2 @@ -9216,7 +8105,7 @@ snapshots: tapable@2.2.2: {} - tapable@2.3.0: {} + tapable@2.3.3: {} tar@7.4.3: dependencies: @@ -9227,21 +8116,21 @@ snapshots: mkdirp: 3.0.1 yallist: 5.0.0 - terser-webpack-plugin@5.3.14(esbuild@0.25.5)(webpack@5.99.9(esbuild@0.25.5)): + terser-webpack-plugin@5.6.1(esbuild@0.25.5)(lightningcss@1.30.1)(webpack@5.99.9(esbuild@0.25.5)(lightningcss@1.30.1)): dependencies: '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 schema-utils: 4.3.3 - serialize-javascript: 6.0.2 - terser: 5.44.1 - webpack: 5.99.9(esbuild@0.25.5) + terser: 5.48.0 + webpack: 5.99.9(esbuild@0.25.5)(lightningcss@1.30.1) optionalDependencies: esbuild: 0.25.5 + lightningcss: 1.30.1 - terser@5.44.1: + terser@5.48.0: dependencies: '@jridgewell/source-map': 0.3.11 - acorn: 8.15.0 + acorn: 8.17.0 commander: 2.20.3 source-map-support: 0.5.21 @@ -9380,9 +8269,9 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 - update-browserslist-db@1.1.4(browserslist@4.28.0): + update-browserslist-db@1.2.3(browserslist@4.28.2): dependencies: - browserslist: 4.28.0 + browserslist: 4.28.2 escalade: 3.2.0 picocolors: 1.1.1 @@ -9402,6 +8291,10 @@ snapshots: dependencies: react: 19.1.0 + use-sync-external-store@1.6.0(react@19.1.0): + dependencies: + react: 19.1.0 + util@0.12.5: dependencies: inherits: 2.0.4 @@ -9414,13 +8307,13 @@ snapshots: uzip@0.20201231.0: {} - vite-node@3.2.4(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.20.3)(yaml@2.8.0): + vite-node@3.2.4(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.48.0)(tsx@4.20.3)(yaml@2.8.0): dependencies: cac: 6.7.14 debug: 4.4.1(supports-color@10.0.0) es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.20.3)(yaml@2.8.0) + vite: 6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.48.0)(tsx@4.20.3)(yaml@2.8.0) transitivePeerDependencies: - '@types/node' - jiti @@ -9435,18 +8328,18 @@ snapshots: - tsx - yaml - vite-plugin-svgr@4.5.0(rollup@4.44.1)(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.20.3)(yaml@2.8.0)): + vite-plugin-svgr@4.5.0(rollup@4.44.1)(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.48.0)(tsx@4.20.3)(yaml@2.8.0)): dependencies: '@rollup/pluginutils': 5.2.0(rollup@4.44.1) '@svgr/core': 8.1.0(typescript@5.8.3) '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@5.8.3)) - vite: 6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.20.3)(yaml@2.8.0) + vite: 6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.48.0)(tsx@4.20.3)(yaml@2.8.0) transitivePeerDependencies: - rollup - supports-color - typescript - vite@6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.20.3)(yaml@2.8.0): + vite@6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.48.0)(tsx@4.20.3)(yaml@2.8.0): dependencies: esbuild: 0.25.5 fdir: 6.4.6(picomatch@4.0.2) @@ -9459,15 +8352,15 @@ snapshots: fsevents: 2.3.3 jiti: 2.4.2 lightningcss: 1.30.1 - terser: 5.44.1 + terser: 5.48.0 tsx: 4.20.3 yaml: 2.8.0 - vitest@3.2.4(@types/node@22.15.34)(@vitest/browser@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.20.3)(yaml@2.8.0): + vitest@3.2.4(@types/node@22.15.34)(@vitest/browser@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.48.0)(tsx@4.20.3)(yaml@2.8.0): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.20.3)(yaml@2.8.0)) + '@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.48.0)(tsx@4.20.3)(yaml@2.8.0)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -9485,12 +8378,12 @@ snapshots: tinyglobby: 0.2.14 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.20.3)(yaml@2.8.0) - vite-node: 3.2.4(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.20.3)(yaml@2.8.0) + vite: 6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.48.0)(tsx@4.20.3)(yaml@2.8.0) + vite-node: 3.2.4(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.48.0)(tsx@4.20.3)(yaml@2.8.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 22.15.34 - '@vitest/browser': 3.2.4(playwright@1.53.2)(vite@6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.20.3)(yaml@2.8.0))(vitest@3.2.4) + '@vitest/browser': 3.2.4(playwright@1.53.2)(vite@6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.48.0)(tsx@4.20.3)(yaml@2.8.0))(vitest@3.2.4) jsdom: 26.1.0 transitivePeerDependencies: - jiti @@ -9514,46 +8407,54 @@ snapshots: dependencies: loose-envify: 1.4.0 - watchpack@2.4.4: + watchpack@2.5.2: dependencies: - glob-to-regexp: 0.4.1 graceful-fs: 4.2.11 webidl-conversions@7.0.0: {} - webpack-sources@3.3.3: {} + webpack-sources@3.5.0: {} webpack-virtual-modules@0.6.2: {} - webpack@5.99.9(esbuild@0.25.5): + webpack@5.99.9(esbuild@0.25.5)(lightningcss@1.30.1): dependencies: '@types/eslint-scope': 3.7.7 - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 '@types/json-schema': 7.0.15 '@webassemblyjs/ast': 1.14.1 '@webassemblyjs/wasm-edit': 1.14.1 '@webassemblyjs/wasm-parser': 1.14.1 - acorn: 8.15.0 - browserslist: 4.28.0 + acorn: 8.17.0 + browserslist: 4.28.2 chrome-trace-event: 1.0.4 - enhanced-resolve: 5.18.3 + enhanced-resolve: 5.24.0 es-module-lexer: 1.7.0 eslint-scope: 5.1.1 events: 3.3.0 glob-to-regexp: 0.4.1 graceful-fs: 4.2.11 json-parse-even-better-errors: 2.3.1 - loader-runner: 4.3.1 + loader-runner: 4.3.2 mime-types: 2.1.35 neo-async: 2.6.2 schema-utils: 4.3.3 - tapable: 2.3.0 - terser-webpack-plugin: 5.3.14(esbuild@0.25.5)(webpack@5.99.9(esbuild@0.25.5)) - watchpack: 2.4.4 - webpack-sources: 3.3.3 + tapable: 2.3.3 + terser-webpack-plugin: 5.6.1(esbuild@0.25.5)(lightningcss@1.30.1)(webpack@5.99.9(esbuild@0.25.5)(lightningcss@1.30.1)) + watchpack: 2.5.2 + webpack-sources: 3.5.0 transitivePeerDependencies: + - '@minify-html/node' - '@swc/core' + - '@swc/css' + - '@swc/html' + - clean-css + - cssnano + - csso - esbuild + - html-minifier-terser + - lightningcss + - postcss - uglify-js webrtc-adapter@9.0.3: diff --git a/apps/web/pnpm-workspace.yaml b/apps/web/pnpm-workspace.yaml new file mode 100644 index 00000000..b2871af4 --- /dev/null +++ b/apps/web/pnpm-workspace.yaml @@ -0,0 +1,4 @@ +ignoreScripts: false +allowBuilds: + '@tailwindcss/oxide': true + esbuild: true diff --git a/apps/web/src/components/AppShell/AdminNavbar.tsx b/apps/web/src/components/AppShell/AdminNavbar.tsx new file mode 100644 index 00000000..7a053e19 --- /dev/null +++ b/apps/web/src/components/AppShell/AdminNavbar.tsx @@ -0,0 +1,58 @@ +import { NavLink } from "@/components/AppShell/NavLink"; +import TablerInfoCircle from "~icons/tabler/info-circle"; +import TablerClipboardCheck from "~icons/tabler/clipboard-check"; +import TablerFileText from "~icons/tabler/file-text"; +import TablerChartBarPopular from "~icons/tabler/chart-bar-popular"; +import TablerSearch from "~icons/tabler/search"; + +interface AdminNavbaPropsProps { + pathname: string; +} + +export default function AdminNavbaProps({ pathname }: AdminNavbaPropsProps) { + const applicationReviewActive = /^\/application-review\/?$/.test(pathname); + const applicationStatisticsActive = /^\/application-statistics\/?$/.test( + pathname, + ); + const applicationSearchActive = /^\/application-search\/?$/.test(pathname); + + const commonNavLinks = ( + <> + } + active={pathname.startsWith("/information")} + /> + } + initialExpanded={ + applicationReviewActive || + applicationStatisticsActive || + applicationSearchActive + } + > + } + active={applicationReviewActive} + /> + } + active={applicationStatisticsActive} + /> + } + active={applicationSearchActive} + /> + + + ); + return commonNavLinks; +} diff --git a/apps/web/src/components/AppShell/AppShell.tsx b/apps/web/src/components/AppShell/AppShell.tsx index 1b4d0180..9b81995f 100644 --- a/apps/web/src/components/AppShell/AppShell.tsx +++ b/apps/web/src/components/AppShell/AppShell.tsx @@ -16,9 +16,6 @@ import IconX from "~icons/tabler/x"; import { auth } from "@/lib/authClient"; import { Profile } from "./Profile"; import { MobileProfile } from "@/components/AppShell/MobileProfile"; -import { Link, useLocation } from "@tanstack/react-router"; -import TablerArrowRight from "~icons/tabler/arrow-right"; -import TablerArrowLeft from "~icons/tabler/arrow-left"; interface AppShellComponent extends FC { Header: FC; @@ -50,12 +47,7 @@ const AppShellBase: FC = ({ children }) => { [children], ); const { data } = auth.useUser(); - const pathname = useLocation({ select: (loc) => loc.pathname }); - - const isAdminPortal = pathname.startsWith("/admin"); - const user = data?.user; - const role = user?.role === "user" ? "Hacker" : "Administrator"; if (data?.error || !user) { return

Something went wrong while loading user information.

; @@ -102,28 +94,7 @@ const AppShellBase: FC = ({ children }) => {
{navbar}
- {user.role === "superuser" && ( -
- {isAdminPortal ? ( - - - Go back to Portal - - ) : ( - - Go to Admin Portal - - - )} -
- )} - +
@@ -139,7 +110,7 @@ const AppShellBase: FC = ({ children }) => { {/* Main content */} {main && ( -
+
{main}
)} diff --git a/apps/web/src/components/AppShell/ApplicantNavbar.tsx b/apps/web/src/components/AppShell/ApplicantNavbar.tsx new file mode 100644 index 00000000..dc39b650 --- /dev/null +++ b/apps/web/src/components/AppShell/ApplicantNavbar.tsx @@ -0,0 +1,38 @@ +import { NavLink } from "@/components/AppShell/NavLink"; +import TablerInfoCircle from "~icons/tabler/info-circle"; +import TablerClipboard from "~icons/tabler/clipboard"; +import TablerAlertCircleFilled from "~icons/tabler/alert-circle-filled"; + +interface ApplicantNavbarProps { + pathname: string; + hasSeenNewApplicationStatus: boolean | null; +} + +export default function ApplicantNavbar({ + pathname, + hasSeenNewApplicationStatus, +}: ApplicantNavbarProps) { + const commonNavLinks = ( + <> + } + active={pathname.startsWith("/information")} + /> + + } + rightSection={ + hasSeenNewApplicationStatus === false && ( + + ) + } + active={pathname.startsWith("/application")} + /> + + ); + return commonNavLinks; +} diff --git a/apps/web/src/components/AppShell/AttendeeNavbar.tsx b/apps/web/src/components/AppShell/AttendeeNavbar.tsx new file mode 100644 index 00000000..1888e999 --- /dev/null +++ b/apps/web/src/components/AppShell/AttendeeNavbar.tsx @@ -0,0 +1,27 @@ +import { NavLink } from "@/components/AppShell/NavLink"; +import TablerLayoutDashboard from "~icons/tabler/layout-dashboard"; +import TablerInfoCircle from "~icons/tabler/info-circle"; + +interface AttendeeNavbarProps { + pathname: string; +} + +export default function AttendeeNavbar({ pathname }: AttendeeNavbarProps) { + const commonNavLinks = ( + <> + } + active={pathname.startsWith("/information")} + /> + } + active={pathname.startsWith("/hacker-portal")} + /> + + ); + return commonNavLinks; +} diff --git a/apps/web/src/components/AppShell/NavLink.tsx b/apps/web/src/components/AppShell/NavLink.tsx index 4d22e7cf..7aea5f8d 100644 --- a/apps/web/src/components/AppShell/NavLink.tsx +++ b/apps/web/src/components/AppShell/NavLink.tsx @@ -1,4 +1,8 @@ -import { useEffect, type PropsWithChildren, type ReactNode } from "react"; +import React, { + useEffect, + type PropsWithChildren, + type ReactNode, +} from "react"; import TablerChevronRight from "~icons/tabler/chevron-right"; import { tv } from "tailwind-variants"; import { useToggleState } from "react-stately"; @@ -19,7 +23,7 @@ const navLink = tv({ interface NavLinkProps { href?: string; - label: string; + label: string | React.ReactNode; description?: string; leftSection?: ReactNode; rightSection?: ReactNode; @@ -63,20 +67,21 @@ const NavLink = ({
{isExpandable ? ( -
- {leftSection && ( - - {leftSection} - - )} -
- {label} - {description && ( - - {description} +
+
+ {leftSection && ( + + {leftSection} )} + {label}
+ + {description && ( + + {description} + + )}
(closeNavbarOnClick ? setMobileNavOpen(false) : null)} > -
- {leftSection && ( - - {leftSection} - - )} -
- {label} - {description && ( - - {description} +
+
+ {leftSection && ( + + {leftSection} )} + {label}
+ + {description && ( + + {description} + + )}
{rightSection && ( diff --git a/apps/web/src/components/AppShell/NavSection.tsx b/apps/web/src/components/AppShell/NavSection.tsx new file mode 100644 index 00000000..da4f3589 --- /dev/null +++ b/apps/web/src/components/AppShell/NavSection.tsx @@ -0,0 +1,12 @@ +interface NavSectionProps { + name: string; +} + +export function NavSection({ name }: NavSectionProps) { + return ( +
+

{name}

+
+
+ ); +} diff --git a/apps/web/src/components/AppShell/Profile.tsx b/apps/web/src/components/AppShell/Profile.tsx index c1e226d7..adb25a10 100644 --- a/apps/web/src/components/AppShell/Profile.tsx +++ b/apps/web/src/components/AppShell/Profile.tsx @@ -3,8 +3,15 @@ import { useAppShell } from "@/components/AppShell/AppShellContext"; import { cn } from "@/utils/cn"; import { useRouter } from "@tanstack/react-router"; import { auth } from "@/lib/authClient"; +import type { UserContext } from "@/lib/auth/types"; -export function Profile({ name, role }: { name: string; role: string }) { +export function Profile({ + name, + role, +}: { + name: string; + role: UserContext["role"]; +}) { const router = useRouter(); const { setMobileNavOpen } = useAppShell(); const { data } = auth.useUser(); @@ -22,7 +29,9 @@ export function Profile({ name, role }: { name: string; role: string }) {

{name}

-

{role}

+

+ {getRoleString(role)} +

); } + +function getRoleString(role: UserContext["role"]) { + switch (role) { + case "admin": + return "Admin"; + case "staff": + return "Staff"; + case "applicant": + return "Applicant"; + case "visitor": + return "Visitor"; + default: + return "Hacker"; + } +} diff --git a/apps/web/src/components/AppShell/StaffNavbar.tsx b/apps/web/src/components/AppShell/StaffNavbar.tsx new file mode 100644 index 00000000..fba4f2d5 --- /dev/null +++ b/apps/web/src/components/AppShell/StaffNavbar.tsx @@ -0,0 +1,53 @@ +import { NavLink } from "@/components/AppShell/NavLink"; +import TablerInfoCircle from "~icons/tabler/info-circle"; +import TablerClipboardCheck from "~icons/tabler/clipboard-check"; +import TablerFileText from "~icons/tabler/file-text"; +import TablerChartBarPopular from "~icons/tabler/chart-bar-popular"; + +interface StaffNavbarProps { + pathname: string; +} + +export default function StaffNavbar({ pathname }: StaffNavbarProps) { + const applicationReviewActive = /^\/application-review\/?$/.test(pathname); + const waitlistActive = /^\/events\/[^/]+\/dashboard\/waitlist\/?$/.test( + pathname, + ); + const applicationStatisticsActive = /^\/application-statistics\/?$/.test( + pathname, + ); + + const commonNavLinks = ( + <> + } + active={pathname.startsWith("/information")} + /> + } + initialExpanded={ + waitlistActive || + applicationReviewActive || + applicationStatisticsActive + } + > + } + active={applicationReviewActive} + /> + } + active={applicationStatisticsActive} + /> + + + ); + return commonNavLinks; +} diff --git a/apps/web/src/components/AppShell/VisitorNavbar.tsx b/apps/web/src/components/AppShell/VisitorNavbar.tsx new file mode 100644 index 00000000..bfd1a4a9 --- /dev/null +++ b/apps/web/src/components/AppShell/VisitorNavbar.tsx @@ -0,0 +1,29 @@ +import { NavLink } from "@/components/AppShell/NavLink"; +import TablerInfoCircle from "~icons/tabler/info-circle"; +import TablerClipboard from "~icons/tabler/clipboard"; + +interface VisitorNavbarProps { + pathname: string; +} + +export default function VisitorNavbar({ pathname }: VisitorNavbarProps) { + const commonNavLinks = ( + <> + } + active={pathname.startsWith("/information")} + /> + + } + active={pathname.startsWith("/application")} + /> + + ); + + return <>{commonNavLinks}; +} diff --git a/apps/web/src/components/ui/Field/Field.tsx b/apps/web/src/components/ui/Field/Field.tsx index 6a4ef1ff..fde85a32 100644 --- a/apps/web/src/components/ui/Field/Field.tsx +++ b/apps/web/src/components/ui/Field/Field.tsx @@ -133,7 +133,6 @@ export const Input = forwardRef( /> ); } - // console.log(iconPlacement); return (
diff --git a/apps/web/src/components/ui/FileField/FileField.tsx b/apps/web/src/components/ui/FileField/FileField.tsx index 50e02f09..831ea7ea 100644 --- a/apps/web/src/components/ui/FileField/FileField.tsx +++ b/apps/web/src/components/ui/FileField/FileField.tsx @@ -2,10 +2,7 @@ // The FileTrigger component doesn't support form submission yet, hence we use a custom file field component here. import { useFormValidation } from "@react-aria/form"; -import { - FormValidationContext, - useFormValidationState, -} from "@react-stately/form"; +import { useFormValidationState } from "@react-stately/form"; import { createContext, type JSX, @@ -15,7 +12,11 @@ import { useMemo, useRef, } from "react"; -import { FieldErrorContext, InputContext } from "react-aria-components"; +import { + FieldErrorContext, + FormValidationContext, + InputContext, +} from "react-aria-components"; import { Description, FieldError, Label } from "@/components/ui/Field"; import { FileInput } from "./FileInput"; import type { ValidationError } from "@tanstack/react-form"; diff --git a/apps/web/src/components/ui/Select/Select.tsx b/apps/web/src/components/ui/Select/Select.tsx index a66e7a17..4e2bc828 100644 --- a/apps/web/src/components/ui/Select/Select.tsx +++ b/apps/web/src/components/ui/Select/Select.tsx @@ -23,7 +23,7 @@ import TablerChevronDown from "~icons/tabler/chevron-down"; import { cn } from "@/utils/cn"; export const styles = tv({ - base: "h-9.5 flex items-center text-start gap-4 w-full cursor-default border border-input-border rounded-sm pl-3 pr-2 py-1.5 min-w-[150px] bg-input-bg", + base: "h-9.5 flex items-center text-start gap-4 w-full cursor-default rounded-sm pl-3 pr-2 py-1.5 min-w-[150px] bg-input-bg", variants: { isDisabled: { false: @@ -73,6 +73,7 @@ export function Select({ props.className, "group flex flex-col gap-1 font-figtree", )} + aria-label="select" // style={{ // maxWidth, // }} diff --git a/apps/web/src/components/ui/Sheet/Sheet.css b/apps/web/src/components/ui/Sheet/Sheet.css new file mode 100644 index 00000000..a798cc0c --- /dev/null +++ b/apps/web/src/components/ui/Sheet/Sheet.css @@ -0,0 +1,67 @@ +.sheet-overlay { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background: color-mix(in oklab, var(--color-black) 15%, transparent); + backdrop-filter: blur(1px); + z-index: 100; + overflow: clip; + + &[data-entering] { + animation: sheet-blur 300ms; + } + + &[data-exiting] { + animation: sheet-blur 300ms reverse ease-in; + } +} + +.sheet { + position: sticky; + left: 0; + /* width: 650px; */ + padding: 5px; + /* Extra padding to account for iOS floating browser UI. */ + top: -100px; + height: calc(100dvh + 200px); + /* padding: 100px 0; */ + margin-left: auto; + background: var(--surface); + outline: none; + /* border-left: 1px solid var(--border); */ + /* box-shadow: -8px 0 20px rgba(0 0 0 / 0.1); */ + font-family: system-ui; + font-size: 0.875rem; + + &[data-entering] { + animation: sheet-slide 300ms; + } + + &[data-exiting] { + animation: sheet-slide 300ms reverse ease-in; + } +} + +@keyframes sheet-blur { + from { + background: rgba(0 0 0 / 0); + backdrop-filter: blur(0); + } + + to { + background: color-mix(in oklab, var(--color-black) 15%, transparent); + backdrop-filter: blur(1px); + } +} + +@keyframes sheet-slide { + from { + transform: translateX(100%); + } + + to { + transform: translateX(0); + } +} diff --git a/apps/web/src/components/ui/Sheet/Sheet.tsx b/apps/web/src/components/ui/Sheet/Sheet.tsx new file mode 100644 index 00000000..e95a93f9 --- /dev/null +++ b/apps/web/src/components/ui/Sheet/Sheet.tsx @@ -0,0 +1,28 @@ +import { + Modal, + ModalOverlay, + type ModalOverlayProps, + Heading, + composeRenderProps, +} from "react-aria-components"; +import { Dialog } from "../Dialog"; +import "./Sheet.css"; +import { cn } from "@/utils/cn"; + +type SheetProps = ModalOverlayProps & { + sheetClassName?: string; +}; + +export function Sheet(props: SheetProps) { + return ( + + {composeRenderProps(props.children, (children) => ( + + {children} + + ))} + + ); +} + +export { Heading }; diff --git a/apps/web/src/components/ui/Sheet/index.ts b/apps/web/src/components/ui/Sheet/index.ts new file mode 100644 index 00000000..f49d7b61 --- /dev/null +++ b/apps/web/src/components/ui/Sheet/index.ts @@ -0,0 +1 @@ +export * from "./Sheet"; diff --git a/apps/web/src/components/ui/Table/Table.tsx b/apps/web/src/components/ui/Table/Table.tsx index aef2b1e9..36796b42 100644 --- a/apps/web/src/components/ui/Table/Table.tsx +++ b/apps/web/src/components/ui/Table/Table.tsx @@ -1,13 +1,4 @@ -import { - flexRender, - getCoreRowModel, - getFilteredRowModel, - getSortedRowModel, - getPaginationRowModel, - useReactTable, - type ColumnDef, - type OnChangeFn, -} from "@tanstack/react-table"; +import { flexRender, type Table } from "@tanstack/react-table"; import { MultiSelect, type MultiSelectProps, @@ -42,246 +33,295 @@ export type PaginationState = { }; interface TableProps { - data: TData[]; - columns: ColumnDef[]; - columnFilters: ColumnFiltersState; - onColumnFiltersChange: OnChangeFn; - sorting: SortingState; - onSortingChange: OnChangeFn; - pagination: PaginationState; - onPaginationChange: OnChangeFn; - fallbackData?: TData[]; + table: Table; + showPagination?: boolean; + headerClassName?: string; + className?: string; } export function Table({ - data, - columns, - columnFilters, - onColumnFiltersChange, - sorting, - onSortingChange, - pagination, - onPaginationChange, - fallbackData = [], + table, + headerClassName, + className, + showPagination = true, }: TableProps) { - const table = useReactTable({ - columns, - data: data ?? fallbackData, - getCoreRowModel: getCoreRowModel(), - getFilteredRowModel: getFilteredRowModel(), - getSortedRowModel: getSortedRowModel(), - getPaginationRowModel: getPaginationRowModel(), - state: { columnFilters, sorting, pagination }, - onColumnFiltersChange: onColumnFiltersChange, - onSortingChange: onSortingChange, - onPaginationChange: onPaginationChange, - }); - return (
- - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { - const sortState = header.column.getIsSorted(); - const responsiveClass = - header.column.columnDef.meta?.responsiveClass ?? ""; - return ( +
+
+ + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( - ); - })} - - ))} - - - {table.getRowModel().rows.map((row, i) => ( - - {row.getVisibleCells().map((cell) => { - const responsiveClass = - cell.column.columnDef.meta?.responsiveClass ?? ""; - return ( + ))} + + ))} + + + {table.getRowModel().rows.map((row, i) => ( + + {row.getVisibleCells().map((cell) => ( - ); - })} - - ))} - -
-
-
- {flexRender( + {header.isPlaceholder + ? null + : flexRender( header.column.columnDef.header, header.getContext(), )} -
-
-
- {header.column.getCanFilter() && - header.column.columnDef.meta?.filterType == - "text" && ( - - header.column.setFilterValue(e) - } - placeholder="Search..." - className="mt-2" - /> - )} - {header.column.getCanFilter() && - header.column.columnDef.meta?.filterType === - "select" && - (() => { - const allOptions = - (header.column.columnDef.meta - ?.filterOptions as MultiSelectProps["options"]) ?? - []; - const filterValue = - (header.column.getFilterValue() ?? - []) as string[]; - const selectedOptions = allOptions.filter((opt) => - filterValue.includes(opt.value), - ); - - return ( -
- { - const newValues = selected.map( - (opt) => opt.value, - ); - header.column.setFilterValue( - newValues.length > 0 - ? newValues - : undefined, - ); - }} - /> -
- ); - })()} -
- {header.column.getCanSort() && ( -
- -
- )} -
-
{flexRender(cell.column.columnDef.cell, cell.getContext())}
-
-
- - - - - -
Page
- - {table.getState().pagination.pageIndex + 1} of{" "} - {table.getPageCount().toLocaleString()} - -
- {/* - | Go to page: - { - const page = e.target.value ? Number(e.target.value) - 1 : 0; - table.setPageIndex(page); - }} - className="border p-1 rounded w-16" - /> - */} - + ))} + + ))} + +
+ {showPagination && ( +
+
+ + + + +
+ + Page {table.getState().pagination.pageIndex + 1} of{" "} + {table.getPageCount().toLocaleString()} + +
+ +
+ +
+ Total Rows: {table.getRowCount()} +
+
+ )}
); + // return ( + //
+ // + // + // {table.getHeaderGroups().map((headerGroup) => ( + // + // {headerGroup.headers.map((header) => { + // // const sortState = header.column.getIsSorted(); + // const responsiveClass = + // header.column.columnDef.meta?.responsiveClass ?? ""; + // return ( + // + // ); + // })} + // + // ))} + // + // + // {table.getRowModel().rows.map((row, i) => ( + // + // {row.getVisibleCells().map((cell) => { + // const responsiveClass = + // cell.column.columnDef.meta?.responsiveClass ?? ""; + // return ( + // + // ); + // })} + // + // ))} + // + //
+ //
+ //
+ // {flexRender( + // header.column.columnDef.header, + // header.getContext(), + // )} + //
+ //
+ //
+ // {header.column.getCanFilter() && + // header.column.columnDef.meta?.filterType == + // "text" && ( + // + // header.column.setFilterValue(e) + // } + // placeholder="Search..." + // className="mt-2" + // /> + // )} + // {header.column.getCanFilter() && + // header.column.columnDef.meta?.filterType === + // "select" && + // (() => { + // const allOptions = + // (header.column.columnDef.meta + // ?.filterOptions as MultiSelectProps["options"]) ?? + // []; + // const filterValue = + // (header.column.getFilterValue() ?? + // []) as string[]; + // const selectedOptions = allOptions.filter((opt) => + // filterValue.includes(opt.value), + // ); + + // return ( + //
+ // { + // const newValues = selected.map( + // (opt) => opt.value, + // ); + // header.column.setFilterValue( + // newValues.length > 0 + // ? newValues + // : undefined, + // ); + // }} + // /> + //
+ // ); + // })()} + //
+ // {/* {header.column.getCanSort() && ( + //
+ // + //
+ // )} */} + //
+ //
+ //
+ // {flexRender(cell.column.columnDef.cell, cell.getContext())} + //
+ + //
+ // ); } diff --git a/apps/web/src/components/ui/Tabs/Tabs.tsx b/apps/web/src/components/ui/Tabs/Tabs.tsx new file mode 100644 index 00000000..4c16138c --- /dev/null +++ b/apps/web/src/components/ui/Tabs/Tabs.tsx @@ -0,0 +1,114 @@ +import { + Tab as RACTab, + TabList as RACTabList, + TabPanels as RACTabPanels, + TabPanel as RACTabPanel, + Tabs as RACTabs, + SelectionIndicator, + type TabListProps, + type TabPanelProps, + type TabPanelsProps, + type TabProps, + type TabsProps, +} from "react-aria-components/Tabs"; +import { composeRenderProps } from "react-aria-components"; +import { tv } from "tailwind-variants"; +import { twMerge } from "tailwind-merge"; + +const tabsStyles = tv({ + base: "flex gap-4 font-sans max-w-full", + variants: { + orientation: { + horizontal: "flex-col", + vertical: "flex-row", + }, + }, +}); + +export function Tabs(props: TabsProps) { + return ( + + tabsStyles({ ...renderProps, className }), + )} + /> + ); +} + +const tabListStyles = tv({ + base: "flex max-w-full p-1 -m-1 overflow-x-auto overflow-y-clip [scrollbar-width:none]", + variants: { + orientation: { + horizontal: "flex-row", + vertical: "flex-col items-start", + }, + }, +}); + +export function TabList(props: TabListProps) { + return ( + + tabListStyles({ ...renderProps, className }), + )} + /> + ); +} + +const tabProps = tv({ + // extend: focusRing, + base: "group relative flex items-center cursor-pointer rounded-full px-3 py-1.5 font-medium transition forced-color-adjust-none [-webkit-tap-highlight-color:transparent]", + variants: { + isDisabled: { + true: "text-neutral-200 dark:text-neutral-600 forced-colors:text-[GrayText] selected:text-white dark:selected:text-neutral-500 forced-colors:selected:text-[HighlightText] selected:bg-neutral-200 dark:selected:bg-neutral-600 forced-colors:selected:bg-[GrayText]", + }, + }, +}); + +export function Tab(props: TabProps) { + return ( + + tabProps({ ...renderProps, className }), + )} + > + {composeRenderProps(props.children, (children) => ( + <> + {children} + + + ))} + + ); +} + +export function TabPanels(props: TabPanelsProps) { + return ( + + ); +} + +const tabPanelStyles = tv({ + // extend: focusRing, + base: "flex-1 box-border text-sm text-neutral-900 dark:text-neutral-100 transition entering:opacity-0 exiting:opacity-0 exiting:absolute exiting:top-0 exiting:left-0 exiting:w-full", +}); + +export function TabPanel(props: TabPanelProps) { + return ( + + tabPanelStyles({ ...renderProps, className }), + )} + /> + ); +} diff --git a/apps/web/src/components/ui/Tabs/index.ts b/apps/web/src/components/ui/Tabs/index.ts new file mode 100644 index 00000000..5de2df69 --- /dev/null +++ b/apps/web/src/components/ui/Tabs/index.ts @@ -0,0 +1 @@ +export * from "./Tabs"; diff --git a/apps/web/src/index.css b/apps/web/src/index.css index 672db8a9..3188a1e0 100644 --- a/apps/web/src/index.css +++ b/apps/web/src/index.css @@ -104,3 +104,21 @@ body, } } } + +/* Target the overall scrollbar width/height */ +::-webkit-scrollbar { + width: 7px; + height:7px; +} + +/* Make the track (background) transparent */ +::-webkit-scrollbar-track { + background: transparent; +} + +/* Style the draggable thumb handle so it stays visible */ +::-webkit-scrollbar-thumb { + background-color: #888; + border-radius: 6px; + border: 3px solid transparent; /* Gives padding if track is transparent */ +} \ No newline at end of file diff --git a/apps/web/src/lib/auth/hooks/useUser.ts b/apps/web/src/lib/auth/hooks/useUser.ts index 17aced69..7a2f4b64 100644 --- a/apps/web/src/lib/auth/hooks/useUser.ts +++ b/apps/web/src/lib/auth/hooks/useUser.ts @@ -1,12 +1,22 @@ -import { useQuery } from "@tanstack/react-query"; +import { queryOptions, useQuery } from "@tanstack/react-query"; import { _getUser } from "../services/user"; -export const queryKey = ["auth", "me"] as const; +export const useUserQueryKey = ["auth", "me"] as const; + +export const userQueryOptions = () => + queryOptions({ + queryKey: useUserQueryKey, + queryFn: async () => await _getUser(), + refetchOnWindowFocus: false, + refetchOnMount: true, + staleTime: 1000 * 60 * 10, // 10 minutes + retry: false, + }); export function _useUser() { // eslint-disable-next-line react-hooks/rules-of-hooks return useQuery({ - queryKey, + queryKey: useUserQueryKey, queryFn: async () => await _getUser(), refetchOnWindowFocus: false, refetchOnMount: true, diff --git a/apps/web/src/lib/auth/services/user.ts b/apps/web/src/lib/auth/services/user.ts index f296c92e..eb61d373 100644 --- a/apps/web/src/lib/auth/services/user.ts +++ b/apps/web/src/lib/auth/services/user.ts @@ -24,8 +24,6 @@ export function _logout(config: AuthConfig) { } export async function _getUser(): Promise { - console.log("fetching user info..."); - try { const res = await fetch(authConfig.AUTH_ME_URL, { credentials: "include", diff --git a/apps/web/src/lib/auth/types/user.ts b/apps/web/src/lib/auth/types/user.ts index b7b2357f..ece045c4 100644 --- a/apps/web/src/lib/auth/types/user.ts +++ b/apps/web/src/lib/auth/types/user.ts @@ -7,10 +7,11 @@ export const userContextSchema = z.object({ name: z.string(), onboarded: z.boolean(), image: z.string().nullable().optional(), - role: z.enum(['admin', 'staff', 'attendee', 'applicant', 'visitor']), + role: z.enum(["admin", "staff", "attendee", "applicant", "visitor"]), emailConsent: z.boolean(), checkedInAt: z.date().nullable(), rfid: z.string().nullable(), + hasSeenNewApplicationStatus: z.boolean().nullable(), }); export type UserContext = z.infer; diff --git a/apps/web/src/lib/authClient.ts b/apps/web/src/lib/authClient.ts index 7e7c69a2..4b1e51c0 100644 --- a/apps/web/src/lib/authClient.ts +++ b/apps/web/src/lib/authClient.ts @@ -2,7 +2,7 @@ import Auth from "./auth"; import { authConfig } from "./auth/config"; import { Discord } from "./auth/providers"; import { queryClient } from "./tanstack-query-client"; -import { queryKey as useUserQueryKey } from "./auth/hooks/useUser"; +import { useUserQueryKey } from "./auth/hooks/useUser"; export const auth = Auth({ providers: [Discord], diff --git a/apps/web/src/lib/openapi/schema.d.ts b/apps/web/src/lib/openapi/schema.d.ts deleted file mode 100644 index 6b011e17..00000000 --- a/apps/web/src/lib/openapi/schema.d.ts +++ /dev/null @@ -1,4858 +0,0 @@ -/** - * This file was auto-generated by openapi-typescript. - * Do not make direct changes to the file. - */ - -export interface paths { - "/application": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get Application - * @description Get the application of the current user - */ - get: operations["get-application"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/application/accept-acceptance": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - /** - * Accept Application Acceptance - * @description Accept an acceptance after being accepted. Sets event role to attendee, from applicant. - */ - patch: operations["accept-application-acceptance"]; - trace?: never; - }; - "/application/assigned": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get Assigned Applications - * @description Returns assigned applications and their review progress for the authenticated reviewer - */ - get: operations["get-assigned-applications"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/application/calculate-admissions": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Submit Admissions Calculation Request - * @description Queues an admission calculation task to the BAT worker - */ - post: operations["calculate-admissions-request"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/application/join-waitlist": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - /** - * Join Waitlist - * @description Adds a waitlist join time to application. Sets status to waitlisted - */ - patch: operations["join-waitlist"]; - trace?: never; - }; - "/application/release-decisions/{runId}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Release Decisions - * @description Releases decisions that were calculated by the worker from a specific run id - */ - post: operations["release-decisions"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/application/resume": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get Resume Download URL - * @description Returns a presigned S3 URL with GET permission for the user's specific object, which is their uploaded resume. The client can use this URL to download the object. - */ - get: operations["get-download-resume-url"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/application/review/assign": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Assign Application Reviewers - * @description Assigns applications to reviewers for the application review process. - */ - post: operations["assign-application-reviewers"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/application/review/reset": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Reset Application Reviews - * @description Resets all application reviews, clearing any existing reviewer assignments. - */ - post: operations["reset-application-reviews"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/application/review/{applicantId}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Submit Application Review - * @description Handles ratings submissions from staff during the application review process - */ - post: operations["submit-application-review"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/application/review/{applicantId}/resume": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get Resume URL (for review process) - * @description Returns a presigned S3 URL with GET permission for a specific user's resume as an object. The client can use this URL to download the object temporarily for application review. - */ - get: operations["get-resume"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/application/save": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Save Application - * @description Save user's progress on the application. File/Upload fields are not saved (eg. resumes). - */ - post: operations["save-application"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/application/stats": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get Application Statistics - * @description Aggregates applications by race, gender, age, majors, and schools - */ - get: operations["get-application-statistics"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/application/submit": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Submit Application - * @description Submit the application - */ - post: operations["submit-application"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/application/transition-waitlisted-applications": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - /** - * Transition Waitlisted Applications - * @description Transitions all accepted users to waitlist, and accepts 50 from the waitlist. Sets application status from accepted to rejected. - */ - patch: operations["transition-waitlist"]; - trace?: never; - }; - "/application/withdraw-acceptance": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - /** - * Withdraw Acceptance - * @description Withdraw an acceptance after being accepted to an event. Sets application status from accepted to rejected. - */ - patch: operations["withdraw-acceptance"]; - trace?: never; - }; - "/application/withdraw-attendance": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - /** - * Withdraw Attendance - * @description Withdraw attendance after accepting to go to the hackathon. Sets application status from accepted to withdrawn. Sets event role from attendee, back to applicant. - */ - patch: operations["withdraw-attendance"]; - trace?: never; - }; - "/auth/callback": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * OAuth Callback - * @description Handles the OAuth provider callback, validates state and nonce, and sets the session cookie. - */ - get: operations["oauth-callback"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/auth/logout": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Logout - * @description Logs out the authenticated user by invalidating their session - */ - post: operations["logout"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/email/queue-confirmation-email": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Queue Confirmation Email - * @description Pushes a confirmation email request to the task queue - */ - post: operations["queue-confirmation-email"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/email/queue-text-email": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Queue Text Email - * @description Pushes a text email request to the task queue - */ - post: operations["queue-text-email"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/email/queue-welcome-email": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Queue Welcome Email - * @description Pushes a welcome email request to the task queue - */ - post: operations["queue-welcome-email"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/email/send-welcome-emails": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Send Welcome Emails - * @description Send welcome emails to all attendees - */ - post: operations["send-welcome-emails"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/hackathon": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get Hackathon - * @description Returns public information of the hackathon - */ - get: operations["get-hackathon"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - /** - * Update Hackathon - * @description Updates the information of the hackathon - */ - patch: operations["update-hackathon"]; - trace?: never; - }; - "/hackathon/attendees/count": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get Hackathon Attendees Count - * @description Returns the number of users who is attending the hackathon - */ - get: operations["get-hackathon-attendees-count"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/hackathon/attendees/discord": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get Hackathon Attendees with Discord - * @description Returns all users with a discord account that is also attending the hackathon - */ - get: operations["get-hackathon-attendees-with-discord"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/hackathon/attendees/userids": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get Hackathon Attendees User Ids - * @description Returns all users ids of users who are attending the hackathon - */ - get: operations["get-hackathon-attendees-userids"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/hackathon/banner": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Upload Banner - * @description Uploads an image to be used as the banner for the hackathon - */ - post: operations["upload-banner"]; - /** - * Delete Banner - * @description Deletes the banner - */ - delete: operations["delete-banner"]; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/hackathon/checkin": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Check In User - * @description Staff route for checking a user to an event. The user to check in must be an attendee and have never been checked in yet. - */ - post: operations["check-in"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/hackathon/detailed": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get Detailed Hackathon - * @description Returns all information of the hackathon - */ - get: operations["get-hackathon-for-staff"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/hackathon/interest": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Submit Interest Email - * @description Submits an email to interest/mailing list for the hackathon - */ - post: operations["submit-interest-email"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/hackathon/staff": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get Hackathon Staff - * @description Returns the users who are part of the current staff of the hackathon - */ - get: operations["get-hackathon-staff"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/ping": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Ping - * @description Health Check - */ - get: operations["ping"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/redeemables": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get Redeemables - * @description Returns a list of all redeemable items - */ - get: operations["get-redeemables"]; - put?: never; - /** - * Create Redeemable - * @description Creates a new redeemable item - */ - post: operations["create-redeemable"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/redeemables/{redeemableId}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - post?: never; - /** - * Delete Redeemable - * @description Deletes a redeemable by id - */ - delete: operations["delete-redeemable"]; - options?: never; - head?: never; - /** - * Update Redeemable - * @description Update specific fields (name, stock, max per user) of a redeemable - */ - patch: operations["update-redeemable"]; - trace?: never; - }; - "/redeemables/{redeemableId}/users/{userID}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Redeem Redeemable - * @description Redeems a redeemable by id. Creates a redemption record linking a specific user to a redeemable item - */ - post: operations["redeem-redeemable"]; - delete?: never; - options?: never; - head?: never; - /** - * Update Redemption - * @description Updates a redemption created by the user. - */ - patch: operations["update-redemption"]; - trace?: never; - }; - "/teams": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Create Team - * @description Creates a new team and assigns the user as the owner. Returns the team. - */ - post: operations["create-team"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/teams/me": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get My Team - * @description Returns the team information and the full list of team members for the currently authenticated user - */ - get: operations["get-my-team"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/teams/me/pending-joins": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get User's Pending Join Requests - * @description Returns the current user's pending requests for teams. - */ - get: operations["get-my-pending-join-requests"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/teams/{requestId}/accept": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Accept Team Join Request - * @description Accepts a pending team join request. Only the team owner can perform this action. - */ - post: operations["accept-team-join-request"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/teams/{requestId}/reject": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Reject Team Join Request - * @description Rejects a pending team join request. Only the team owner can perform this action. - */ - post: operations["reject-team-join-request"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/teams/{teamId}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get Team - * @description Returns the team information and the full list of team members by team id - */ - get: operations["get-team"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/teams/{teamId}/join": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Request to Join Team - * @description Requests to join a team or fails if user is already on a team. - */ - post: operations["create-join-team-request"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/teams/{teamId}/kick/{memberId}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Kick Team Member - * @description Kicks a member from a team. Only the team owner can perform this action. - */ - post: operations["kick-member-from-team"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/teams/{teamId}/leave": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Leave Team - * @description Leaves a team if the user is on the team. - */ - post: operations["leave-team"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/teams/{teamId}/pending-joins": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get Pending Join Requests for Team - * @description Returns a team's pending join requests. This is only allowed for the team's owner. - */ - get: operations["get-pending-join-team-requests"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/users": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get Users - * @description Get or search for users by name or email. If no search term is provided, returns all users with pagination. - */ - get: operations["get-users"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/users/email/{email}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get User By Email - * @description Returns the user associated with the email - */ - get: operations["get-user-by-email"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/users/me": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get Me - * @description Returns the authenticated user's profile - */ - get: operations["get-me"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - /** - * Update User - * @description Updates information of the authenticated user - */ - patch: operations["update-user"]; - trace?: never; - }; - "/users/me/email-consent": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - /** - * Update Email Consent - * @description Updates the user's email consent setting - */ - patch: operations["update-email-consent"]; - trace?: never; - }; - "/users/me/onboarding": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - /** - * Onboard User - * @description Allows the user to submit information such as name and preferred email, and complete the onboarding process - */ - patch: operations["onboard-user"]; - trace?: never; - }; - "/users/rfid/{rfid}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get User By RFID - * @description Returns the user associated with the RFID - */ - get: operations["get-user-by-rfid"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/users/roles/assign": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Assign Role - * @description Assigns/modify a user's role - */ - post: operations["assign-role"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/users/roles/batch-assign": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Batch Assign Roles - * @description Batch assign/modify multiple users' roles - */ - post: operations["batch-assign-roles"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/users/roles/revoke/{userID}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Revoke Role - * @description Remove a user's role - */ - post: operations["revoke-role"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/users/userid/{userID}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get User By Id - * @description Returns the user associated with the user id - */ - get: operations["get-user-by-id"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; -} -export type webhooks = Record; -export interface components { - schemas: { - ApplicationStatistics: { - ageStats: components["schemas"]["GetApplicationAgeSplitRow"]; - genderStats: components["schemas"]["GetApplicationGenderSplitRow"]; - majorStats: components["schemas"]["GetApplicationMajorSplitRow"][] | null; - raceStats: components["schemas"]["GetApplicationRaceSplitRow"][] | null; - schoolStats: - | components["schemas"]["GetApplicationSchoolSplitRow"][] - | null; - statusStats: components["schemas"]["GetApplicationStatusSplitRow"]; - }; - AssignRoleBatchRequest: { - assignments: components["schemas"]["AssignRoleRequest"][] | null; - }; - AssignRoleRequest: { - email: string | null; - role: string; - userID: string | null; - }; - AssignedApplication: { - applicantId: string; - status: string; - }; - CheckInRequest: { - rfid: string | null; - userID: string; - }; - CreateJoinRequest: { - message: string | null; - }; - CreateRedeemableRequest: { - /** Format: int64 */ - amount: number; - /** Format: int64 */ - maxUserAmount: number; - name: string; - }; - CreateTeamRequest: { - name: string; - }; - ErrorDetail: { - /** @description Where the error occurred, e.g. 'body.items[3].tags' or 'path.thing-id' */ - location?: string; - /** @description Error message text */ - message?: string; - /** @description The value at the given location */ - value?: unknown; - }; - ErrorModel: { - /** @description A human-readable explanation specific to this occurrence of the problem. */ - detail?: string; - /** @description Optional list of individual error details */ - errors?: components["schemas"]["ErrorDetail"][] | null; - /** - * Format: uri - * @description A URI reference that identifies the specific occurrence of the problem. - */ - instance?: string; - /** - * Format: int64 - * @description HTTP status code - */ - status?: number; - /** @description A short, human-readable summary of the problem type. This value should not change between occurrences of the error. */ - title?: string; - /** - * Format: uri - * @description A URI reference to human-readable documentation for the error. - * @default about:blank - */ - type: string; - }; - FormFile: { - ContentType: string; - Filename: string; - IsSet: boolean; - /** Format: int64 */ - Size: number; - }; - GetApplicationAgeSplitRow: { - /** Format: int64 */ - age_18: number; - /** Format: int64 */ - age_19: number; - /** Format: int64 */ - age_20: number; - /** Format: int64 */ - age_21: number; - /** Format: int64 */ - age_22: number; - /** Format: int64 */ - age_23_plus: number; - /** Format: int64 */ - underage: number; - }; - GetApplicationGenderSplitRow: { - /** Format: int64 */ - female: number; - /** Format: int64 */ - male: number; - /** Format: int64 */ - non_binary: number; - /** Format: int64 */ - other: number; - }; - GetApplicationMajorSplitRow: { - /** Format: int64 */ - count: number; - major: string; - }; - GetApplicationRaceSplitRow: { - /** Format: int64 */ - count: number; - race_group: string; - }; - GetApplicationSchoolSplitRow: { - /** Format: int64 */ - count: number; - school: string; - }; - GetApplicationStatusSplitRow: { - /** Format: int64 */ - accepted: number; - /** Format: int64 */ - rejected: number; - /** Format: int64 */ - started: number; - /** Format: int64 */ - submitted: number; - /** Format: int64 */ - under_review: number; - /** Format: int64 */ - waitlisted: number; - /** Format: int64 */ - withdrawn: number; - }; - GetAttendeesWithDiscordRow: { - discord_id: string; - email: string | null; - name: string; - user_id: string; - }; - GetRedeemablesRow: { - /** Format: date-time */ - created_at: string; - id: string; - /** Format: int32 */ - max_user_amount: number; - name: string; - total_redeemed: unknown; - /** Format: int32 */ - total_stock: number; - /** Format: date-time */ - updated_at: string; - }; - Hackathon: { - accept_early_applications: boolean; - /** Format: date-time */ - application_close: string; - /** Format: date-time */ - application_open: string; - application_review_started: boolean; - banner: string | null; - /** Format: date-time */ - created_at: string; - /** Format: date-time */ - decision_release: string | null; - description: string | null; - /** Format: date-time */ - early_application_close: string | null; - /** Format: date-time */ - early_application_open: string | null; - /** Format: date-time */ - end_time: string; - id: string; - is_active: boolean; - location: string | null; - location_url: string | null; - /** Format: int32 */ - max_attendees: number | null; - name: string; - /** Format: date-time */ - rsvp_deadline: string | null; - /** Format: date-time */ - start_time: string; - /** Format: date-time */ - updated_at: string; - }; - HackerApplication: { - application: string; - /** Format: date-time */ - createdAt: string; - hackathonId: string; - /** Format: date-time */ - savedAt: string; - status: string; - /** Format: date-time */ - submittedAt: string | null; - /** Format: date-time */ - updatedAt: string; - userId: string; - }; - ListJoinRequestsByTeamAndStatusWithUserRow: { - /** Format: date-time */ - created_at: string; - id: string; - /** Format: date-time */ - processed_at: string | null; - processed_by_user_id: string; - request_message: string | null; - status: string; - team_id: string; - /** Format: date-time */ - updated_at: string; - user_email: string | null; - user_id: string; - user_image: string | null; - user_name: string; - }; - MemberWithUserInfo: { - email: string | null; - image: string | null; - /** Format: date-time */ - joinedAt: string; - name: string; - userID: string; - }; - OnboardingRequest: { - name: string; - preferredEmail: string; - }; - PublicHackathon: { - acceptEarlyApplications: boolean; - /** Format: date-time */ - applicationClose: string; - /** Format: date-time */ - applicationOpen: string; - banner: string | null; - description: string | null; - /** Format: date-time */ - earlyApplicationClose: string | null; - /** Format: date-time */ - earlyApplicationOpen: string | null; - /** Format: date-time */ - endTime: string; - id: string; - location: string | null; - locationUrl: string | null; - name: string; - /** Format: date-time */ - rsvpDeadline: string | null; - /** Format: date-time */ - startTime: string; - }; - QueueConfirmationEmailRequest: { - email: string; - firstName: string; - }; - QueueTextEmailRequest: { - body: string; - subject: string; - to: string[] | null; - }; - QueueWelcomeEmailRequest: { - email: string; - firstName: string; - recipientId: string; - }; - Redeemable: { - /** Format: int32 */ - amount: number; - /** Format: date-time */ - created_at: string; - hackathon_id: string; - id: string; - /** Format: int32 */ - max_user_amount: number; - name: string; - /** Format: date-time */ - updated_at: string; - }; - ReviewRatings: { - /** Format: int64 */ - experienceRating: number; - /** Format: int64 */ - passionRating: number; - }; - ReviewerAssignment: { - /** Format: int64 */ - amount: number | null; - userID: string; - }; - SubmitInterestEmailRequest: { - email: string; - source: string | null; - }; - Team: { - /** Format: date-time */ - created_at: string; - hackathon_id: string; - id: string; - name: string; - owner_id: string; - /** Format: date-time */ - updated_at: string; - }; - TeamJoinRequest: { - /** Format: date-time */ - created_at: string; - id: string; - /** Format: date-time */ - processed_at: string | null; - processed_by_user_id: string; - request_message: string | null; - status: string; - team_id: string; - /** Format: date-time */ - updated_at: string; - user_id: string; - }; - TeamWithMembers: { - id: string; - members: components["schemas"]["MemberWithUserInfo"][] | null; - name: string; - ownerId: string; - }; - UpdateEmailConsentRequest: { - emailConsent: boolean; - }; - UpdateHackathonRequest: { - /** Format: date-time */ - applicationClose?: string; - /** Format: date-time */ - applicationOpen?: string; - /** Format: date-time */ - decisionRelease?: string | null; - description?: string | null; - /** Format: date-time */ - endTime?: string; - location?: string | null; - locationUrl?: string | null; - /** Format: int32 */ - maxAttendees?: number | null; - name?: string; - /** Format: date-time */ - rsvpDeadline?: string | null; - /** Format: date-time */ - startTime?: string; - }; - UpdateRedeemableRequest: { - /** Format: int64 */ - maxUserAmount?: number; - name?: string; - /** Format: int64 */ - totalStock?: number; - }; - UpdateRedemptionRequest: { - /** Format: int64 */ - newAmount?: number; - }; - UpdateUserRequest: { - name: string; - preferredEmail: string; - }; - User: { - /** Format: date-time */ - checked_in_at: string | null; - /** Format: date-time */ - created_at: string; - email: string | null; - email_consent: boolean; - email_verified: boolean; - id: string; - image: string | null; - name: string; - onboarded: boolean; - preferred_email: string | null; - rfid: string | null; - role: string; - /** Format: date-time */ - role_assigned_at: string | null; - /** Format: date-time */ - updated_at: string; - }; - UserContext: { - /** Format: date-time */ - checkedInAt: string | null; - email: string | null; - emailConsent: boolean; - image: string | null; - name: string; - onboarded: boolean; - preferredEmail: string | null; - rfid: string | null; - /** @enum {string} */ - role: "admin" | "staff" | "attendee" | "applicant" | "visitor"; - /** Format: uuid */ - userId: string; - }; - }; - responses: never; - parameters: never; - requestBodies: never; - headers: never; - pathItems: never; -} -export type $defs = Record; -export interface operations { - "get-application": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie: { - /** @description Session cookie used to authenticate the user */ - sh_session_id: string; - }; - }; - requestBody?: never; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HackerApplication"]; - }; - }; - /** @description Unauthorized */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Internal Server Error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - }; - }; - "accept-application-acceptance": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie: { - /** @description Session cookie used to authenticate the user */ - sh_session_id: string; - }; - }; - requestBody?: never; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Unauthorized */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Internal Server Error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - }; - }; - "get-assigned-applications": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie: { - /** @description Session cookie used to authenticate the user */ - sh_session_id: string; - }; - }; - requestBody?: never; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": - | components["schemas"]["AssignedApplication"][] - | null; - }; - }; - /** @description Unauthorized */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Internal Server Error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - }; - }; - "calculate-admissions-request": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie: { - /** @description Session cookie used to authenticate the user */ - sh_session_id: string; - }; - }; - requestBody?: never; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Unauthorized */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Internal Server Error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - }; - }; - "join-waitlist": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie: { - /** @description Session cookie used to authenticate the user */ - sh_session_id: string; - }; - }; - requestBody?: never; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Unauthorized */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Internal Server Error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - }; - }; - "release-decisions": { - parameters: { - query?: never; - header?: never; - path: { - runId: string; - }; - cookie: { - /** @description Session cookie used to authenticate the user */ - sh_session_id: string; - }; - }; - requestBody?: never; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Unauthorized */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Unprocessable Entity */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Internal Server Error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - }; - }; - "get-download-resume-url": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie: { - /** @description Session cookie used to authenticate the user */ - sh_session_id: string; - }; - }; - requestBody?: never; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": string; - }; - }; - /** @description Unauthorized */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Internal Server Error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - }; - }; - "assign-application-reviewers": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie: { - /** @description Session cookie used to authenticate the user */ - sh_session_id: string; - }; - }; - requestBody: { - content: { - "application/json": - | components["schemas"]["ReviewerAssignment"][] - | null; - }; - }; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Unauthorized */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Unprocessable Entity */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Internal Server Error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - }; - }; - "reset-application-reviews": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie: { - /** @description Session cookie used to authenticate the user */ - sh_session_id: string; - }; - }; - requestBody?: never; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Unauthorized */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Internal Server Error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - }; - }; - "submit-application-review": { - parameters: { - query?: never; - header?: never; - path: { - applicantId: string; - }; - cookie: { - /** @description Session cookie used to authenticate the user */ - sh_session_id: string; - }; - }; - requestBody: { - content: { - "application/json": components["schemas"]["ReviewRatings"]; - }; - }; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Unauthorized */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Unprocessable Entity */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Internal Server Error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - }; - }; - "get-resume": { - parameters: { - query?: never; - header?: never; - path: { - applicantId: string; - }; - cookie: { - /** @description Session cookie used to authenticate the user */ - sh_session_id: string; - }; - }; - requestBody?: never; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": string; - }; - }; - /** @description Unauthorized */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Unprocessable Entity */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Internal Server Error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - }; - }; - "save-application": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie: { - /** @description Session cookie used to authenticate the user */ - sh_session_id: string; - }; - }; - requestBody?: { - content: { - "application/json": unknown; - }; - }; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Unauthorized */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Unprocessable Entity */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Internal Server Error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - }; - }; - "get-application-statistics": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie: { - /** @description Session cookie used to authenticate the user */ - sh_session_id: string; - }; - }; - requestBody?: never; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ApplicationStatistics"]; - }; - }; - /** @description Unauthorized */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Internal Server Error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - }; - }; - "submit-application": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie: { - /** @description Session cookie used to authenticate the user */ - sh_session_id: string; - }; - }; - requestBody?: never; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Unauthorized */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Internal Server Error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - }; - }; - "transition-waitlist": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie: { - /** @description Session cookie used to authenticate the user */ - sh_session_id: string; - }; - }; - requestBody?: never; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Unauthorized */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Internal Server Error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - }; - }; - "withdraw-acceptance": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie: { - /** @description Session cookie used to authenticate the user */ - sh_session_id: string; - }; - }; - requestBody?: never; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Unauthorized */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Internal Server Error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - }; - }; - "withdraw-attendance": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie: { - /** @description Session cookie used to authenticate the user */ - sh_session_id: string; - }; - }; - requestBody?: never; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Unauthorized */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Internal Server Error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - }; - }; - "oauth-callback": { - parameters: { - query: { - /** @description OAuth authorization code */ - code: string; - /** @description Base64 encoded OAuth state */ - state: string; - }; - header?: { - /** @description Client user agent */ - "User-Agent"?: string; - }; - path?: never; - cookie: { - /** @description Auth nonce cookie for CSRF protection */ - sh_auth_nonce: string; - }; - }; - requestBody?: never; - responses: { - /** @description No Content */ - 204: { - headers: { - Location?: string; - "Set-Cookie"?: string; - [name: string]: unknown; - }; - content?: never; - }; - /** @description Bad Request */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Unauthorized */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Unprocessable Entity */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Internal Server Error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Not Implemented */ - 501: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - }; - }; - logout: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description No Content */ - 204: { - headers: { - "Set-Cookie"?: string; - [name: string]: unknown; - }; - content?: never; - }; - /** @description Unauthorized */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Internal Server Error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - }; - }; - "queue-confirmation-email": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["QueueConfirmationEmailRequest"]; - }; - }; - responses: { - /** @description No Content */ - 204: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Bad Request */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Unauthorized */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Unprocessable Entity */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Internal Server Error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - }; - }; - "queue-text-email": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["QueueTextEmailRequest"]; - }; - }; - responses: { - /** @description No Content */ - 204: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Bad Request */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Unauthorized */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Unprocessable Entity */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Internal Server Error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - }; - }; - "queue-welcome-email": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["QueueWelcomeEmailRequest"]; - }; - }; - responses: { - /** @description No Content */ - 204: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Bad Request */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Unauthorized */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Unprocessable Entity */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Internal Server Error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - }; - }; - "send-welcome-emails": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description No Content */ - 204: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Unauthorized */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Internal Server Error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - }; - }; - "get-hackathon": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie: { - /** @description Session cookie used to authenticate the user */ - sh_session_id: string; - }; - }; - requestBody?: never; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["PublicHackathon"]; - }; - }; - /** @description Unauthorized */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Not Found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Internal Server Error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - }; - }; - "update-hackathon": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie: { - /** @description Session cookie used to authenticate the user */ - sh_session_id: string; - }; - }; - requestBody: { - content: { - "application/json": components["schemas"]["UpdateHackathonRequest"]; - }; - }; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Unauthorized */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Unprocessable Entity */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Internal Server Error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - }; - }; - "get-hackathon-attendees-count": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie: { - /** @description Session cookie used to authenticate the user */ - sh_session_id: string; - }; - }; - requestBody?: never; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": number; - }; - }; - /** @description Unauthorized */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Internal Server Error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - }; - }; - "get-hackathon-attendees-with-discord": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie: { - /** @description Session cookie used to authenticate the user */ - sh_session_id: string; - }; - }; - requestBody?: never; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": - | components["schemas"]["GetAttendeesWithDiscordRow"][] - | null; - }; - }; - /** @description Unauthorized */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Internal Server Error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - }; - }; - "get-hackathon-attendees-userids": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie: { - /** @description Session cookie used to authenticate the user */ - sh_session_id: string; - }; - }; - requestBody?: never; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": string[] | null; - }; - }; - /** @description Unauthorized */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Internal Server Error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - }; - }; - "upload-banner": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie: { - /** @description Session cookie used to authenticate the user */ - sh_session_id: string; - }; - }; - requestBody?: { - content: { - "multipart/form-data": { - /** Format: binary */ - image: string; - }; - }; - }; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": string | null; - }; - }; - /** @description Bad Request */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Unprocessable Entity */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Internal Server Error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - }; - }; - "delete-banner": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie: { - /** @description Session cookie used to authenticate the user */ - sh_session_id: string; - }; - }; - requestBody?: never; - responses: { - /** @description No Content */ - 204: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Bad Request */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Internal Server Error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - }; - }; - "check-in": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie: { - /** @description Session cookie used to authenticate the user */ - sh_session_id: string; - }; - }; - requestBody: { - content: { - "application/json": components["schemas"]["CheckInRequest"]; - }; - }; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Unauthorized */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Unprocessable Entity */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Internal Server Error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - }; - }; - "get-hackathon-for-staff": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie: { - /** @description Session cookie used to authenticate the user */ - sh_session_id: string; - }; - }; - requestBody?: never; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["Hackathon"]; - }; - }; - /** @description Unauthorized */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Not Found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Internal Server Error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - }; - }; - "submit-interest-email": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["SubmitInterestEmailRequest"]; - }; - }; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Bad Request */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Unprocessable Entity */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Internal Server Error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - }; - }; - "get-hackathon-staff": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie: { - /** @description Session cookie used to authenticate the user */ - sh_session_id: string; - }; - }; - requestBody?: never; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["User"][] | null; - }; - }; - /** @description Unauthorized */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Internal Server Error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - }; - }; - ping: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": string; - }; - }; - /** @description Error */ - default: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - }; - }; - "get-redeemables": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie: { - /** @description Session cookie used to authenticate the user */ - sh_session_id: string; - }; - }; - requestBody?: never; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": - | components["schemas"]["GetRedeemablesRow"][] - | null; - }; - }; - /** @description Unauthorized */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Internal Server Error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - }; - }; - "create-redeemable": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie: { - /** @description Session cookie used to authenticate the user */ - sh_session_id: string; - }; - }; - requestBody: { - content: { - "application/json": components["schemas"]["CreateRedeemableRequest"]; - }; - }; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["Redeemable"]; - }; - }; - /** @description Unauthorized */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Unprocessable Entity */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Internal Server Error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - }; - }; - "delete-redeemable": { - parameters: { - query?: never; - header?: never; - path: { - redeemableId: string; - }; - cookie: { - /** @description Session cookie used to authenticate the user */ - sh_session_id: string; - }; - }; - requestBody?: never; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Bad Request */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Unauthorized */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Unprocessable Entity */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Internal Server Error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - }; - }; - "update-redeemable": { - parameters: { - query?: never; - header?: never; - path: { - redeemableId: string; - }; - cookie: { - /** @description Session cookie used to authenticate the user */ - sh_session_id: string; - }; - }; - requestBody: { - content: { - "application/json": components["schemas"]["UpdateRedeemableRequest"]; - }; - }; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Bad Request */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Unauthorized */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Unprocessable Entity */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Internal Server Error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - }; - }; - "redeem-redeemable": { - parameters: { - query?: never; - header?: never; - path: { - redeemableId: string; - }; - cookie: { - /** @description Session cookie used to authenticate the user */ - sh_session_id: string; - }; - }; - requestBody?: never; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Bad Request */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Unauthorized */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Unprocessable Entity */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Internal Server Error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - }; - }; - "update-redemption": { - parameters: { - query?: never; - header?: never; - path: { - redeemableId: string; - }; - cookie: { - /** @description Session cookie used to authenticate the user */ - sh_session_id: string; - }; - }; - requestBody: { - content: { - "application/json": components["schemas"]["UpdateRedemptionRequest"]; - }; - }; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Bad Request */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Unauthorized */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Unprocessable Entity */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Internal Server Error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - }; - }; - "create-team": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie: { - /** @description Session cookie used to authenticate the user */ - sh_session_id: string; - }; - }; - requestBody: { - content: { - "application/json": components["schemas"]["CreateTeamRequest"]; - }; - }; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["Team"]; - }; - }; - /** @description Bad Request */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Unauthorized */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Not Found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Conflict */ - 409: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Unprocessable Entity */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Internal Server Error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - }; - }; - "get-my-team": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie: { - /** @description Session cookie used to authenticate the user */ - sh_session_id: string; - }; - }; - requestBody?: never; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["TeamWithMembers"]; - }; - }; - /** @description Unauthorized */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Not Found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Internal Server Error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - }; - }; - "get-my-pending-join-requests": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie: { - /** @description Session cookie used to authenticate the user */ - sh_session_id: string; - }; - }; - requestBody?: never; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["TeamJoinRequest"][] | null; - }; - }; - /** @description Bad Request */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Unauthorized */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Internal Server Error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - }; - }; - "accept-team-join-request": { - parameters: { - query?: never; - header?: never; - path: { - requestId: string; - }; - cookie: { - /** @description Session cookie used to authenticate the user */ - sh_session_id: string; - }; - }; - requestBody?: never; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Bad Request */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Unauthorized */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Unprocessable Entity */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Internal Server Error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - }; - }; - "reject-team-join-request": { - parameters: { - query?: never; - header?: never; - path: { - requestId: string; - }; - cookie: { - /** @description Session cookie used to authenticate the user */ - sh_session_id: string; - }; - }; - requestBody?: never; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Bad Request */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Unauthorized */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Unprocessable Entity */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Internal Server Error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - }; - }; - "get-team": { - parameters: { - query?: never; - header?: never; - path: { - teamId: string; - }; - cookie: { - /** @description Session cookie used to authenticate the user */ - sh_session_id: string; - }; - }; - requestBody?: never; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["TeamWithMembers"]; - }; - }; - /** @description Bad Request */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Unauthorized */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Not Found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Unprocessable Entity */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Internal Server Error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - }; - }; - "create-join-team-request": { - parameters: { - query?: never; - header?: never; - path: { - teamId: string; - }; - cookie: { - /** @description Session cookie used to authenticate the user */ - sh_session_id: string; - }; - }; - requestBody: { - content: { - "application/json": components["schemas"]["CreateJoinRequest"]; - }; - }; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["TeamJoinRequest"]; - }; - }; - /** @description Bad Request */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Unauthorized */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Conflict */ - 409: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Unprocessable Entity */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Internal Server Error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - }; - }; - "kick-member-from-team": { - parameters: { - query?: never; - header?: never; - path: { - memberId: string; - teamId: string; - }; - cookie: { - /** @description Session cookie used to authenticate the user */ - sh_session_id: string; - }; - }; - requestBody?: never; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Bad Request */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Unauthorized */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Unprocessable Entity */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Internal Server Error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - }; - }; - "leave-team": { - parameters: { - query?: never; - header?: never; - path: { - teamId: string; - }; - cookie: { - /** @description Session cookie used to authenticate the user */ - sh_session_id: string; - }; - }; - requestBody?: never; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Bad Request */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Unauthorized */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Unprocessable Entity */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Internal Server Error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - }; - }; - "get-pending-join-team-requests": { - parameters: { - query?: never; - header?: never; - path: { - teamId: string; - }; - cookie: { - /** @description Session cookie used to authenticate the user */ - sh_session_id: string; - }; - }; - requestBody?: never; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": - | components["schemas"]["ListJoinRequestsByTeamAndStatusWithUserRow"][] - | null; - }; - }; - /** @description Bad Request */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Unauthorized */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Forbidden */ - 403: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Unprocessable Entity */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Internal Server Error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - }; - }; - "get-users": { - parameters: { - query?: { - search?: string; - limit?: number; - offset?: number; - }; - header?: never; - path?: never; - cookie: { - /** @description Session cookie used to authenticate the user */ - sh_session_id: string; - }; - }; - requestBody?: never; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["User"][] | null; - }; - }; - /** @description Bad Request */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Unauthorized */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Not Found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Unprocessable Entity */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Internal Server Error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - }; - }; - "get-user-by-email": { - parameters: { - query?: never; - header?: never; - path: { - email: string; - }; - cookie: { - /** @description Session cookie used to authenticate the user */ - sh_session_id: string; - }; - }; - requestBody?: never; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["User"]; - }; - }; - /** @description Bad Request */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Unauthorized */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Not Found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Unprocessable Entity */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Internal Server Error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - }; - }; - "get-me": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie: { - /** @description Session cookie used to authenticate the user */ - sh_session_id: string; - }; - }; - requestBody?: never; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["UserContext"]; - }; - }; - /** @description Unauthorized */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Internal Server Error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - }; - }; - "update-user": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie: { - /** @description Session cookie used to authenticate the user */ - sh_session_id: string; - }; - }; - requestBody: { - content: { - "application/json": components["schemas"]["UpdateUserRequest"]; - }; - }; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Bad Request */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Unauthorized */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Not Found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Unprocessable Entity */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Internal Server Error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - }; - }; - "update-email-consent": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie: { - /** @description Session cookie used to authenticate the user */ - sh_session_id: string; - }; - }; - requestBody: { - content: { - "application/json": components["schemas"]["UpdateEmailConsentRequest"]; - }; - }; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Bad Request */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Unauthorized */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Not Found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Unprocessable Entity */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Internal Server Error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - }; - }; - "onboard-user": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie: { - /** @description Session cookie used to authenticate the user */ - sh_session_id: string; - }; - }; - requestBody: { - content: { - "application/json": components["schemas"]["OnboardingRequest"]; - }; - }; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Bad Request */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Unauthorized */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Not Found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Unprocessable Entity */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Internal Server Error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - }; - }; - "get-user-by-rfid": { - parameters: { - query?: never; - header?: never; - path: { - rfid: string; - }; - cookie: { - /** @description Session cookie used to authenticate the user */ - sh_session_id: string; - }; - }; - requestBody?: never; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["User"]; - }; - }; - /** @description Bad Request */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Unauthorized */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Not Found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Unprocessable Entity */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Internal Server Error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - }; - }; - "assign-role": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie: { - /** @description Session cookie used to authenticate the user */ - sh_session_id: string; - }; - }; - requestBody: { - content: { - "application/json": components["schemas"]["AssignRoleRequest"]; - }; - }; - responses: { - /** @description No Content */ - 204: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Bad Request */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Unauthorized */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Unprocessable Entity */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Internal Server Error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - }; - }; - "batch-assign-roles": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie: { - /** @description Session cookie used to authenticate the user */ - sh_session_id: string; - }; - }; - requestBody: { - content: { - "application/json": components["schemas"]["AssignRoleBatchRequest"]; - }; - }; - responses: { - /** @description No Content */ - 204: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Bad Request */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Unauthorized */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Unprocessable Entity */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Internal Server Error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - }; - }; - "revoke-role": { - parameters: { - query?: never; - header?: never; - path: { - userID: string; - }; - cookie: { - /** @description Session cookie used to authenticate the user */ - sh_session_id: string; - }; - }; - requestBody?: never; - responses: { - /** @description No Content */ - 204: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Bad Request */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Unauthorized */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Unprocessable Entity */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Internal Server Error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - }; - }; - "get-user-by-id": { - parameters: { - query?: never; - header?: never; - path: { - userID: string; - }; - cookie: { - /** @description Session cookie used to authenticate the user */ - sh_session_id: string; - }; - }; - requestBody?: never; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["User"]; - }; - }; - /** @description Bad Request */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Unauthorized */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Not Found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Unprocessable Entity */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - /** @description Internal Server Error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["ErrorModel"]; - }; - }; - }; - }; -} diff --git a/apps/web/src/lib/openapi/types.ts b/apps/web/src/lib/openapi/types.ts deleted file mode 100644 index db38df5a..00000000 --- a/apps/web/src/lib/openapi/types.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { components } from "./schema"; - -export type UserContext = components["schemas"]["UserContext"]; -export type Application = components["schemas"]["HackerApplication"]; -export type Hackathon = components["schemas"]["PublicHackathon"]; diff --git a/apps/web/src/lib/openapi/zodSchemas.ts b/apps/web/src/lib/openapi/zodSchemas.ts deleted file mode 100644 index b7505804..00000000 --- a/apps/web/src/lib/openapi/zodSchemas.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Zod schemas for OpenAPI specification components. - * This file needs to be manually kept in sync with the OpenAPI schema definitions. - */ - -import { z } from "zod"; - -export const PlatformRoleSchema = z.enum(["user", "superuser"]); -export const EventRoleSchema = z.enum([ - "attendee", - "admin", - "staff", - "applicant", -]); diff --git a/apps/web/src/modules/Application/ApplicationForm.tsx b/apps/web/src/modules/Application/ApplicationForm.tsx index e580a66b..05b744cf 100644 --- a/apps/web/src/modules/Application/ApplicationForm.tsx +++ b/apps/web/src/modules/Application/ApplicationForm.tsx @@ -3,9 +3,11 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { QuestionTypes } from "@/modules/FormBuilder/types"; import { showToast } from "@/lib/toast/toast"; import TablerCircleCheck from "~icons/tabler/circle-check"; +import TablerUpload from "~icons/tabler/upload"; import { api } from "@/lib/ky"; import { Spinner } from "@/components/ui/Spinner"; -import { useMyApplication } from "@/modules/Application/hooks/useMyApplication"; +import { Button } from "@/components/ui/Button"; +import { useReplaceResume } from "@/modules/Application/hooks/useReplaceResume"; import { formatDistanceToNowStrict, parseISO } from "date-fns"; import Cloud from "./assets/cloud.svg?react"; @@ -17,16 +19,23 @@ import Bell from "./assets/bell.svg?react"; // TODO: dynamically fetch application json data from somewhere (backend, cdn?) instead of hardcoding it in the frontend import data from "./application.json"; -import type { Hackathon } from "@/lib/openapi/types"; import { HTTPError } from "ky"; +import type { Hackathon } from "@/modules/Hackathon/hooks/useHackathon"; +import type { Application } from "@/modules/Application/hooks/useApplication"; const SAVE_DELAY_MS = 3000; // delay in time before saving form progress interface ApplicationFormProps { hackathon: Hackathon; + application: Application; + applicationResponses: any; } -export function ApplicationForm({ hackathon }: ApplicationFormProps) { +export function ApplicationForm({ + hackathon, + application, + applicationResponses, +}: ApplicationFormProps) { // TODO: make the `build` api better so components that use this function doesn't have to call useMemo on it? const { Form, fieldsTypes } = useMemo(() => build(data), []); const fileFields = useRef(new Set()); @@ -38,8 +47,6 @@ export function ApplicationForm({ hackathon }: ApplicationFormProps) { const [savedText, setSavedText] = useState(""); const [submittedAt, setSubmittedAt] = useState(undefined); - const application = useMyApplication(); - // Update saved text every second. Restart interval when lastSavedAt changes. useEffect(() => { const id = setInterval(() => { @@ -55,15 +62,15 @@ export function ApplicationForm({ hackathon }: ApplicationFormProps) { // Update saved at status message, if any. useEffect(() => { - if (!application || application.isLoading) return; + if (!application) return; - if (application.data?.savedAt) { - const parsed = parseISO(application.data.savedAt); + if (application?.savedAt) { + const parsed = parseISO(application.savedAt); setLastSavedAt(parsed); } else { setLastSavedAt(undefined); } - }, [application?.data?.savedAt, application?.isLoading]); + }, [application?.savedAt]); const onSubmit = useCallback(async (data: Record) => { setIsSubmitting(true); @@ -140,20 +147,7 @@ export function ApplicationForm({ hackathon }: ApplicationFormProps) { [isSubmitted, isSubmitting], ); - if (application.isLoading) { - return ( -
- -

Loading form...

-
- ); - } - - if (!application.data) { - throw new Error("Application data is empty."); - } - - const isApplicationSubmitted = application.data.status !== "started"; + const isApplicationSubmitted = application.status !== "started"; const saveStatus = ( <> @@ -175,9 +169,7 @@ export function ApplicationForm({ hackathon }: ApplicationFormProps) {
( )} isInvalid={isInvalid} @@ -233,7 +225,7 @@ export function ApplicationForm({ hackathon }: ApplicationFormProps) { function SubmitSuccess({ submittedAt }: { submittedAt: string }) { return (
-
+

Thank you! Your application has been received.

@@ -247,6 +239,78 @@ function SubmitSuccess({ submittedAt }: { submittedAt: string }) { minute: "2-digit", }).format(new Date(submittedAt))}

+ +
+ ); +} + +function ReplaceResume() { + const inputRef = useRef(null); + const { mutate: replaceResume, isPending } = useReplaceResume(); + + const onSelectFile = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + // Reset so selecting the same file again still fires onChange + e.target.value = ""; + + if (!file) return; + + if (file.type !== "application/pdf") { + showToast({ + title: "Invalid file", + message: "Please upload a PDF resume.", + type: "error", + }); + return; + } + + replaceResume(file, { + onSuccess: () => { + showToast({ + title: "Resume updated", + message: "Your resume has been replaced successfully.", + type: "success", + }); + }, + onError: async (err) => { + let message = "Something went wrong while replacing your resume."; + if (err instanceof HTTPError) { + const resBody = await err.response.json<{ detail?: string }>(); + message = resBody.detail || message; + } + + showToast({ + title: "Upload failed", + message, + type: "error", + }); + }, + }); + }; + + return ( +
+

+ Uploaded the wrong resume? You can replace it below. This does not + change any of your other application responses. +

+ +
); } diff --git a/apps/web/src/modules/Application/ApplicationReview/ApplicationReviewPage.tsx b/apps/web/src/modules/Application/ApplicationReview/ApplicationReviewPage.tsx new file mode 100644 index 00000000..4b47ac82 --- /dev/null +++ b/apps/web/src/modules/Application/ApplicationReview/ApplicationReviewPage.tsx @@ -0,0 +1,296 @@ +import { Button } from "@/components/ui/Button"; +import { Modal } from "@/components/ui/Modal"; +import { Table } from "@/components/ui/Table"; +import type { UserContext } from "@/lib/auth/types"; +import ReviewerAssignmentModal from "@/modules/Application/ApplicationReview/ReviewersAssignmentModal"; +import ReviewNotStartedAdmin from "@/modules/Application/ApplicationReview/ReviewNotStartedAdmin"; +import { useApplicationReviewAdminActions } from "@/modules/Application/hooks/useApplicationReviewAdminActions"; +import { Link } from "@tanstack/react-router"; +import { useMemo, useState } from "react"; +import { DialogTrigger } from "react-aria-components"; +import TablerBrowserShare from "~icons/tabler/browser-share"; +import TablerUserEdit from "~icons/tabler/user-edit"; +import { + getCoreRowModel, + useReactTable, + type ColumnDef, +} from "@tanstack/react-table"; +import { useReviewersProgress } from "@/modules/Application/hooks/useReviewersProgress"; +import { Input } from "@/components/ui/Field"; +import { Select } from "@/components/ui/Select"; +import TablerSearch from "~icons/tabler/search"; +import type { components } from "@/lib/openapi/schema"; +import { useDebounce } from "@uidotdev/usehooks"; +import AutoDecisionRequestList from "@/modules/Application/ApplicationReview/AutoDecisionRequestList"; + +interface ApplicationReviewPageProps { + hackathon: components["schemas"]["Hackathon"]; + user: UserContext; +} + +export default function ApplicationReviewPage({ + hackathon, + user, +}: ApplicationReviewPageProps) { + const { updateStatus } = useApplicationReviewAdminActions(); + + const handleStartApplicationReview = async () => { + await updateStatus.mutateAsync(true); + }; + + const handleEndApplicationReview = async () => { + if (window.confirm("Are you sure you want to end application review?")) { + await updateStatus.mutateAsync(false); + } + }; + + if (!hackathon.application_review_started) { + if (user.role === "admin") { + return ( + + ); + } else { + return ( +

+ Application review has not started. +

+ ); + } + } + + return ( +
+
+ + + + {user.role === "admin" && ( + + )} +
+ + {user.role === "admin" && ( +
+ + +
+ )} +
+ ); +} + +function AutoDecisionRequestsTable() { + const [searchInput, setSearchInput] = useState(""); + const debouncedSearchInput = useDebounce(searchInput, 500); + + const [approvalFilter, setApprovalFilter] = useState("all"); + const [decisionFilter, setDecisionFilter] = useState("all"); + + return ( +
+
+

Auto Decision Requests

+
+ +
+
+
+ setSearchInput(e.target.value)} + /> +
+
+ Decision Filter: + { + if (key) setApprovalFilter(key.toString()); + }} + children={null} + /> +
+
+
+ + +
+ ); +} + +function Reviewers() { + const { data, isLoading, isError } = useReviewersProgress(); + + const reviewers = data ?? []; + const columns: ColumnDef<(typeof reviewers)[number]>[] = useMemo( + () => [ + { + header: "Reviewer", + size: 190, + cell: ({ row }) => { + const avatarUrl = row.original.image; + return ( +
+ {avatarUrl ? ( + {"user + ) : ( +
+ + N/A + +
+ )} + + {row.original.name} + +
+ ); + }, + }, + { + header: "Assigned", + size: 80, + cell: ({ row }) => row.original.totalAssigned, + }, + { + header: "Completed", + size: 60, + cell: ({ row }) => row.original.completedCount, + }, + ], + [], + ); + + const table = useReactTable({ + columns, + data: reviewers, + getCoreRowModel: getCoreRowModel(), + }); + + if (isLoading) { + return

Loading reviewers progress...

; + } + + if (isError) { + return

Unable to load reviewers progress data.

; + } + + const totalApps = reviewers.reduce((prev, curr) => { + return curr.totalAssigned + prev; + }, 0); + + const completed = reviewers.reduce((prev, curr) => { + return curr.completedCount + prev; + }, 0); + + return ( +
+
+

Reviewers

+ + {reviewers.length} reviewer{reviewers.length === 1 ? "" : "s"} + +
+ + {reviewers.length === 0 ? ( +

+ No reviewers were assigned. +

+ ) : ( + + )} + +
+
+

Applications under review: {totalApps}

+

Reviews completed: {completed}

+
+ 0} + /> +
+ + ); +} + +function AssignReviewers({ + alreadyAssigned, + text = "Assign Reviewers", +}: { + text?: string; + alreadyAssigned: boolean; +}) { + const [open, setOpen] = useState(false); + + return ( + + + {open && ( + + setOpen(false)} + /> + + )} + + ); +} diff --git a/apps/web/src/modules/Application/ApplicationReview/AutoDecisionRequestList.tsx b/apps/web/src/modules/Application/ApplicationReview/AutoDecisionRequestList.tsx new file mode 100644 index 00000000..9aeb9e3d --- /dev/null +++ b/apps/web/src/modules/Application/ApplicationReview/AutoDecisionRequestList.tsx @@ -0,0 +1,259 @@ +import { Button } from "@/components/ui/Button"; +import { Popover } from "@/components/ui/Popover"; +import { Sheet } from "@/components/ui/Sheet"; +import { Table } from "@/components/ui/Table"; +import ApplicationViewer from "@/modules/Application/ApplicationViewer"; +import { useUpdateAutoDecisionRequest } from "@/modules/Application/hooks/useAutoDecisionRequests"; +import useParsedForm from "@/modules/Application/hooks/useParsedForm"; +import { + useSearchAutoDecisionRequests, + type SearchAutoDecisionRequestsResponse, +} from "@/modules/Application/hooks/useSearchAutoDecisionRequests"; +import { + type ColumnDef, + useReactTable, + getCoreRowModel, + getFilteredRowModel, +} from "@tanstack/react-table"; +import { useState, useMemo } from "react"; +import { DialogTrigger } from "react-aria-components"; + +interface AutoDecisionRequestListProps { + searchInput: string; + approvalFilter: string; + decisionFilter: string; +} + +export default function AutoDecisionRequestList({ + searchInput, + approvalFilter, + decisionFilter, +}: AutoDecisionRequestListProps) { + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 10, + }); + + const searchAutoDecisionRequestsData = useSearchAutoDecisionRequests( + pagination.pageSize, + pagination.pageIndex, + searchInput, + approvalFilter, + decisionFilter, + ); + + const updateRequest = useUpdateAutoDecisionRequest(); + const parsedForm = useParsedForm(); + + const requestRows = + searchAutoDecisionRequestsData.data?.autoDecisionRequests ?? []; + + type AutoDecisionRequest = + SearchAutoDecisionRequestsResponse["autoDecisionRequests"][number]; + + const columns: ColumnDef[] = useMemo( + () => [ + { + id: "applicant", + header: "Applicant", + accessorKey: "user.name", + size: 200, + cell: ({ row }) => { + const avatarUrl = row.original.user.image; + return ( +
+ {avatarUrl ? ( + {"user + ) : ( +
+ + N/A + +
+ )} + + {row.original.user.name} + +
+ ); + }, + }, + { + id: "reviewer", + header: "Reviewer", + accessorKey: "reviewer.name", + size: 200, + cell: ({ row }) => { + const avatarUrl = row.original.reviewer.image; + return ( +
+ {avatarUrl ? ( + {"user + ) : ( +
+ + N/A + +
+ )} + + {row.original.reviewer.name} + +
+ ); + }, + }, + { + header: "Application", + size: 90, + cell: ({ row }) => ( + + + + + + + ), + }, + { + header: "Decision", + accessorKey: "requestedDecision", + enableGlobalFilter: false, + size: 130, + cell: ({ row }) => + row.original.requestedDecision === "auto_accept" + ? "Auto Accept" + : "Auto Reject", + }, + { + header: "Justification", + enableGlobalFilter: false, + size: 110, + cell: ({ row }) => ( + + + +
+

+ {row.original.justification} +

+
+
+
+ ), + }, + { + header: "Created At", + size: 200, + cell: ({ row }) => new Date(row.original.createdAt).toLocaleString(), + }, + { + header: "Actions", + id: "actions", + size: 150, + accessorFn: (row) => + row.decidedBy ? (row.approved ? "approved" : "denied") : "pending", + enableGlobalFilter: false, + cell: ({ row }) => { + const isResolved = row.original.decidedBy !== null; + + return isResolved ? ( + + {row.original.approved ? "Approved" : "Denied"} + + ) : ( +
+ + +
+ ); + }, + }, + { + header: "Decided By", + accessorKey: "decidedBy", + enableGlobalFilter: false, + size: 180, + cell: ({ row }) => ( + + {row.original.decidedBy?.name} + + ), + }, + ], + [parsedForm], + ); + + const table = useReactTable({ + globalFilterFn: "includesString", + columns, + data: requestRows, + rowCount: searchAutoDecisionRequestsData.data?.count, + state: { pagination, globalFilter: searchInput }, + getCoreRowModel: getCoreRowModel(), + onPaginationChange: setPagination, + getFilteredRowModel: getFilteredRowModel(), + manualPagination: true, + manualFiltering: true, + }); + + if (searchAutoDecisionRequestsData.isLoading) { + return

Loading requests....

; + } + + if (!searchAutoDecisionRequestsData.data) { + return

Unable to load requests.

; + } + + return requestRows.length === 0 ? ( +
+

No requests found.

+
+ ) : ( +
+ ); +} diff --git a/apps/web/src/modules/ApplicationReview/ResetReviewModal.tsx b/apps/web/src/modules/Application/ApplicationReview/ResetReviewModal.tsx similarity index 100% rename from apps/web/src/modules/ApplicationReview/ResetReviewModal.tsx rename to apps/web/src/modules/Application/ApplicationReview/ResetReviewModal.tsx diff --git a/apps/web/src/modules/Application/ApplicationReview/ReviewNotStartedAdmin.tsx b/apps/web/src/modules/Application/ApplicationReview/ReviewNotStartedAdmin.tsx new file mode 100644 index 00000000..046ac4e0 --- /dev/null +++ b/apps/web/src/modules/Application/ApplicationReview/ReviewNotStartedAdmin.tsx @@ -0,0 +1,50 @@ +import { useApplicationStatistics } from "@/modules/Application/hooks/useApplicationStatistics"; +import { Button } from "@/components/ui/Button"; +import type { StaffHackathon } from "@/modules/Hackathon/hooks/useHackathon"; + +interface ReviewNotStartedProps { + hackathon: StaffHackathon; + startApplicationReview: () => void; +} + +export default function ReviewNotStartedAdmin({ + hackathon, + startApplicationReview, +}: ReviewNotStartedProps) { + const stats = useApplicationStatistics(); + + if (stats.isLoading || !stats.data) { + return
Loading...
; + } + + const now = new Date(); + const applicationPeriodClosed = now >= new Date(hackathon.application_close); + const validNumOfApplicants = stats.data.statusStats.submitted > 0; + + return ( +
+

Application review has not started.

+ + + + {!applicationPeriodClosed ? ( +

+ The application period is still ongoing. +

+ ) : ( + !validNumOfApplicants && ( +

+ There are no submitted applications to review yet. +

+ ) + )} +
+ ); +} diff --git a/apps/web/src/modules/Application/ApplicationReview/ReviewersAssignmentModal.tsx b/apps/web/src/modules/Application/ApplicationReview/ReviewersAssignmentModal.tsx new file mode 100644 index 00000000..1881ca70 --- /dev/null +++ b/apps/web/src/modules/Application/ApplicationReview/ReviewersAssignmentModal.tsx @@ -0,0 +1,199 @@ +import { useState } from "react"; +import { Button } from "@/components/ui/Button"; +import { + useApplicationReviewAdminActions, + type AssignedReviewer, +} from "../hooks/useApplicationReviewAdminActions"; +import { toast } from "react-toastify"; +import { useApplicationStatistics } from "@/modules/Application/hooks/useApplicationStatistics"; +import { + useHackathonStaff, + type HackathonStaff, +} from "@/modules/Hackathon/hooks/useHackathonStaff"; +import { Checkbox, CheckboxGroup } from "@/components/ui/Checkbox"; +import { NumberField, Input } from "react-aria-components"; +import { Tooltip } from "@/components/ui/Tooltip"; +import TablerHelpCircle from "~icons/tabler/help-circle"; +import type { Dispatch, SetStateAction } from "react"; + +interface Props { + onClose: () => void; + alreadyAssigned: boolean; +} + +export default function ReviewerAssignmentModal({ + onClose, + alreadyAssigned, +}: Props) { + const [assigned, setAssigned] = useState([]); + const { assign } = useApplicationReviewAdminActions(); + const staff = useHackathonStaff(); + const stats = useApplicationStatistics(); + + if (staff.isLoading || !staff.data) { + return

Loading staff...

; + } + + if (stats.isLoading || !stats.data) { + return
Loading application stats...
; + } + + const numAssigned = assigned.reduce( + (acc, curr) => acc + (curr.amount ?? 0), + 0, + ); + + const remaining = stats.data.statusStats.under_review - numAssigned; + + const handleSubmit = async () => { + const hasNull = assigned.some((r) => r.amount === null); + const hasZero = assigned.some((r) => r.amount === 0); + const hasNegative = assigned.some((r) => (r.amount ?? 0) < 0); + + if (hasNegative) + return alert( + "Assigned applications cannot be negative for any reviewer.", + ); + if (hasZero) + return alert( + "Assigned applications cannot be zero for any reviewer. Either leave blank or assign a positive number.", + ); + if (remaining < 0) return alert("Assigned applications exceed total."); + if (remaining > 0 && !hasNull) + return alert( + "Leave at least one reviewer blank to distribute remaining.", + ); + if (assigned.length === 0) return alert("Select reviewers."); + + let confirmed; + if (alreadyAssigned) { + confirmed = window.confirm( + "Re-assigning will delete all application reviews and auto decision requests. Are you sure you want to proceed?", + ); + } + + if (confirmed || !alreadyAssigned) { + await assign.mutateAsync(assigned, { + onSuccess: () => { + toast.success("Reviewers assigned successfully."); + onClose(); + }, + onError: () => { + toast.error("Failed to assign reviewers. Please try again."); + }, + }); + } + }; + + return ( +
+

Select Reviewers

+
+ +
+
+
+

Assigned Applications: {numAssigned}

+

Total Applications: {stats.data.statusStats.under_review}

+

Remaining: {stats.data.statusStats.under_review - numAssigned}

+
+
+ + +
+
+
+ ); +} + +interface ReviewerListProps { + staff: HackathonStaff; + assigned: AssignedReviewer[]; + setAssigned: Dispatch>; +} + +function ReviewerList({ staff, assigned, setAssigned }: ReviewerListProps) { + const handleSelectionChange = (selected: string[]) => { + const updated = selected.map((userId) => { + const existing = assigned.find((r) => r.userId === userId); + return existing ?? { userId, amount: null }; + }); + + setAssigned(updated); + }; + + return ( + <> +
+

Reviewer

+ +
+ + + +

# of Apps

+
+
+ + + {staff.map((s) => { + const isSelected = assigned.some((a) => a.userId === s.id); + + return ( +
+ +
+ {s.name || "Anonymous"} + + {s.email} + +
+
+ + + { + const value = e.target.value; + setAssigned((prev) => + prev.map((r) => + r.userId === s.id + ? { + ...r, + amount: value === "" ? null : Number(value), + } + : r, + ), + ); + }} + className="border-border border-1 disabled:border-0 w-12 h-6 text-sm px-1 disabled:bg-input-bg-disbaled dark:disabled:bg-neutral-800 disabled:cursor-not-allowed" + /> + +
+ ); + })} +
+ + ); +} diff --git a/apps/web/src/modules/Application/ApplicationReview/Workspace.tsx b/apps/web/src/modules/Application/ApplicationReview/Workspace.tsx new file mode 100644 index 00000000..2889684d --- /dev/null +++ b/apps/web/src/modules/Application/ApplicationReview/Workspace.tsx @@ -0,0 +1,703 @@ +import type { UserContext } from "@/lib/auth/types"; +import { Link } from "@tanstack/react-router"; +import { useRatings } from "@/modules/Application/hooks/useRatings"; +import { + useReviewAssignments, + type ReviewAssignments, +} from "@/modules/Application/hooks/useReviewAssignments"; +import { useAppReviewProgress } from "@/modules/Application/hooks/useAppReviewProgress"; +import { + useApplicationReview, + type ParsedApplicationReview, +} from "@/modules/Application/hooks/useApplicationReview"; +import { useApplicationReviewActions } from "@/modules/Application/hooks/useApplicationReviewActions"; +import { toast } from "react-toastify"; +import { ProgressBar } from "@/components/ui/ProgressBar"; +import TablerX from "~icons/tabler/x"; +import { useState } from "react"; +import { DialogTrigger, TextArea } from "react-aria-components"; +import { Modal } from "@/components/ui/Modal"; +import { Input } from "@/components/ui/Field"; +import TablerCheck from "~icons/tabler/check"; +import TablerArrowLeft from "~icons/tabler/arrow-left"; +import TablerArrowRight from "~icons/tabler/arrow-right"; +import TablerRefresh from "~icons/tabler/refresh"; +import { Button } from "@/components/ui/Button"; +import type { ApplicationFields } from "@/modules/Application/hooks/useApplication"; + +interface ApplicationReviewWorkspaceProps { + user: UserContext; +} + +export default function ApplicationReviewWorkspace({ + user, +}: ApplicationReviewWorkspaceProps) { + const assignments = useReviewAssignments(); + const appReviewProgress = useAppReviewProgress(assignments.data || []); + + if (assignments.isLoading) { + return ( +
+

Loading assigned applications...

+
+ ); + } + + if (!assignments.data || assignments.data.length === 0) { + return ( +
+

+ You have no assigned applications to review at this time. +
+ Refresh the page or ask the organizers if you believe this is an + error. +

+
+ ); + } + + if (appReviewProgress.finished) { + return ( +
+
+
+ Review Completed +
+ +

Reviews Completed

+ +

+ You have completed reviewing all assigned applications. Thank you + for your time and effort! You can go back to change your reviews if + needed. +

+ +
+ +
+
+
+ ); + } + + return ( +
+
+
+ + + +

SwampHacks Application Review

+
+ +
+ +
+
+ + {appReviewProgress.currentAssignment ? ( + + ) : ( +
Loading application...
+ )} +
+ ); +} + +interface ApplicationViewerProps { + user: UserContext; + assignment: ReviewAssignments[number]; + currentIndex: number; + totalApplications: number; + next: () => void; + back: () => void; +} + +function ApplicationViewer({ + user, + assignment, + totalApplications, + currentIndex, + next, + back, +}: ApplicationViewerProps) { + const applicationReview = useApplicationReview(assignment.reviewId); + + if (!applicationReview.data || applicationReview.isLoading) { + return

Loading...

; + } + + const appFields = applicationReview.data.application; + const resume = applicationReview.data.resumeUrl; + + return ( +
+
+ + +
+ +
+
+ {resume === "" ? ( +

No resume provided.

+ ) : ( + +

+ Your browser does not support PDFs.{" "} + Download the PDF. +

+
+ )} +
+ + +
+
+ ); +} + +interface ApplicantInfoProps { + appFields: ApplicationFields; +} + +function ApplicantInfo({ appFields }: ApplicantInfoProps) { + const getHackathonExperienceText = (experience: string) => { + switch (experience) { + case "first_time": + return "Swamphacks would be my first!"; + case "one": + return "1"; + case "two": + return "2"; + case "three": + return "3"; + case "four_or_more": + return "4+"; + default: + return ""; + } + }; + + const getProjectExperienceText = (experience: string) => { + switch (experience) { + case "no_experience": + return "Swamphacks would be my first!"; + case "course_experience": + return "From courses"; + case "independent_project": + return "Yes"; + default: + return ""; + } + }; + + return ( +
+

Applicant Information

+
+
+
+
Name
+
+ {appFields.firstName + " " + appFields.lastName} +
+
+ +
+
Major(s)
+
{appFields.majors}
+
+ +
+
School
+
{appFields.school}
+
+ +
+
Graduation Year
+
{appFields.graduationYear}
+
+
+ +
+
+
# of Hackathons Attended
+
+ {getHackathonExperienceText(appFields.experience)} +
+
+
+
Project Experience
+
+ {getProjectExperienceText(appFields.projectExperience)} +
+
+
+
+
+ ); +} + +interface EssaysProps { + appFields: ApplicationFields; +} + +function Essays({ appFields }: EssaysProps) { + return ( +
+
+

Essay Responses

+
+
+
+ What is your most memorable experience working in a group? What + did you learn and accomplish? +
+
+ {appFields.essay1} +
+
+ +
+
+ Tell us about a project you are most proud of. +
+
+ {appFields.essay2} +
+
+
+
+
+ ); +} + +interface ReviewerPanelProps { + user: UserContext; + assignment: ReviewAssignments[number]; + applicationReview: ParsedApplicationReview; + totalApplications: number; + currentIndex: number; + next: () => void; + back: () => void; +} + +function ReviewerPanel({ + user, + applicationReview, + assignment, + totalApplications, + currentIndex, + next, + back, +}: ReviewerPanelProps) { + const { review, requestAutoDecision, deleteAutoDecisionRequest } = + useApplicationReviewActions(assignment.applicationId, assignment.reviewId); + const { + experience, + passion, + isDirty, + setExperience, + setPassion, + reset: resetRatings, + } = useRatings( + applicationReview.experienceRating || 0, + applicationReview.passionRating || 0, + ); + const [notes, setNotes] = useState(applicationReview.notes || ""); + + const autoDecisionRequest = applicationReview.autoDecisionRequest; + const isCompleted = assignment.status === "completed"; + const isLast = currentIndex === totalApplications - 1; + const isFilled = experience > 0 && passion > 0; + const allowSubmit = isFilled && isDirty; + + const mode = (() => { + if (!isCompleted) return "submit"; // Not last, not submitted yet + if (isCompleted && isDirty) return "completed-dirty"; + return "completed-clean"; // Not last, already submitted and clean + })(); + + const handleSubmitReview = async () => { + if (!allowSubmit) return; + + await review.mutateAsync( + { + reviewId: assignment.reviewId, + experienceRating: experience, + passionRating: passion, + notes, + }, + { + onSuccess: () => { + resetRatings(); + next(); + }, + onError: () => { + toast.error("Failed to submit review. Please try again."); + resetRatings(); + }, + }, + ); + }; + + const handleRequestAutoAccept = async (justification: string) => { + await requestAutoDecision.mutateAsync({ + applicationId: assignment.applicationId, + accept: true, + justification, + }); + }; + + const handleRequestAutoReject = async (justification: string) => { + await requestAutoDecision.mutateAsync({ + applicationId: assignment.applicationId, + accept: false, + justification, + }); + }; + + const handleUndoAutoDecision = async () => { + if (!autoDecisionRequest) return; + + await deleteAutoDecisionRequest.mutateAsync({ + requestId: autoDecisionRequest.id, + }); + }; + + return ( +
+

+ Rubric and Controls +

+
+ +
+