From c05ffe59ba32e267bf38ce39918face93ff847e0 Mon Sep 17 00:00:00 2001 From: h1divp <71522316+h1divp@users.noreply.github.com> Date: Thu, 26 Jun 2025 17:43:56 -0400 Subject: [PATCH 01/52] fix: makefile points to correct env file --- apps/api/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/api/Makefile b/apps/api/Makefile index 944ce490..820dad83 100644 --- a/apps/api/Makefile +++ b/apps/api/Makefile @@ -1,4 +1,4 @@ -include .env.local +include .env.dev migrate-up: @goose -dir ./internal/db/migrations postgres ${DATABASE_URL_MIGRATIONS} up From a30d7babff48497cd8d4218a11a19ffaef610ecb Mon Sep 17 00:00:00 2001 From: h1divp <71522316+h1divp@users.noreply.github.com> Date: Thu, 26 Jun 2025 17:54:46 -0400 Subject: [PATCH 02/52] docs: added discord dev portal redirect uri instruction --- apps/docs/src/api/installation.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/apps/docs/src/api/installation.md b/apps/docs/src/api/installation.md index 0283f2ea..d6840531 100644 --- a/apps/docs/src/api/installation.md +++ b/apps/docs/src/api/installation.md @@ -3,23 +3,25 @@ ### Setup with Docker Compose (main setup) 1. Navigate to `core/apps/api` -2. **Set up environment variables**: +1. **Set up environment variables**: ``` bash cp .env.dev.example .env.dev ``` -3. Open `.env.dev` +1. Open `.env.dev` 1. For `AUTH_DISCORD_CLIENT_ID` and `AUTH_DISCORD_CLIENT_SECRET`, go to the Discord developer portal and create an account. Create a new application and go to the OAuth2 tab in the left sidebar. Copy the Client ID and the Client Secret into their respective environment variables. - 2. Fill out any other required keys and tokens, if empty. + 1. While in the OAuth2 menu, copy the `AUTH_DISCORD_REDIRECT_URI` parameter from the example configuration and paste into the box under the *Redirects* header. This is the URL which discord will redirect the user to after Discord authentication has completed. -3. Continue with the [main setup instructions](../getting-started.md) + 1. Fill out any other required keys and tokens, if empty. + +1. Continue with the [main setup instructions](../getting-started.md) ### Setup without Docker Compose 1. Make sure you have [Go](https://go.dev/) installed on your system. -2. Initialize the Go project +1. Initialize the Go project ``` bash go mod tidy ``` @@ -29,7 +31,7 @@ go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest go install github.com/pressly/goose/v3/cmd/goose@latest ``` -3. Run the program with +1. Run the program with ```bash air ``` From 0a42fd1987272a18abcccb4b80388f3154f805ac Mon Sep 17 00:00:00 2001 From: h1divp <71522316+h1divp@users.noreply.github.com> Date: Thu, 26 Jun 2025 18:58:30 -0400 Subject: [PATCH 03/52] fix: env variable name change --- apps/api/Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/api/Makefile b/apps/api/Makefile index 820dad83..84d5b63d 100644 --- a/apps/api/Makefile +++ b/apps/api/Makefile @@ -1,10 +1,10 @@ include .env.dev migrate-up: - @goose -dir ./internal/db/migrations postgres ${DATABASE_URL_MIGRATIONS} up + @goose -dir ./internal/db/migrations postgres ${DATABASE_URL_MIGRATION} up migrate-down: - @goose -dir ./internal/db/migrations postgres ${DATABASE_URL_MIGRATIONS} down + @goose -dir ./internal/db/migrations postgres ${DATABASE_URL_MIGRATION} down generate: @sqlc generate From bfadb562219e8ef085af6633749e7a712dfb8c77 Mon Sep 17 00:00:00 2001 From: h1divp <71522316+h1divp@users.noreply.github.com> Date: Tue, 1 Jul 2025 21:48:07 -0400 Subject: [PATCH 04/52] feat: added event object queries --- apps/api/internal/db/queries/events.sql | 92 +++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 apps/api/internal/db/queries/events.sql diff --git a/apps/api/internal/db/queries/events.sql b/apps/api/internal/db/queries/events.sql new file mode 100644 index 00000000..8c7dde7b --- /dev/null +++ b/apps/api/internal/db/queries/events.sql @@ -0,0 +1,92 @@ +-- name: CreateEvent :one +INSERT INTO events ( + id, name, description, + location, loction_url, max_attendees, + application_open, application_close, rsvp_deadline, decision_release, + start_time, end_time, + website_url +) VALUES ( + $1, $2, $3, + $4, $5, $6, + $7, $8, $9, $10, + $11, $12, + $13 +) +RETURNING *; + +-- name: GetEventByID: one +SELECT * FROM events +WHERE id = $1; + +-- name: GetEventByLocation: many +SELECT * FROM events +WHERE location = $1; + +-- name: UpdateEventName: exec +UPDATE events +SET name = $2 +WHERE id = $1; + +-- name: UpdateEventDescription: exec +UPDATE events +SET description = $2 +WHERE id = $1; + +-- name: UpdateEventLocation: exec +UPDATE events +SET location = $2 +WHERE id = $1; + +-- name: UpdateEventLocationUrl: exec +UPDATE events +SET location_url = $2 +WHERE id = $1; + +-- name: UpdateEventMaxAttendees: exec +UPDATE events +SET max_attendees = $2 +WHERE id = $1; + +-- name: UpdateEventApplicationOpen: exec +UPDATE events +SET application_open = $2 +WHERE id = $1; + +-- name: UpdateEventApplicationClose: exec +UPDATE events +SET application_close = $2 +WHERE id = $1; + +-- name: UpdateEventRsvpDeadline: exec +UPDATE events +SET rsvp_deadline = $2 +WHERE id = $1; + +-- name: UpdateEventDecisionRelease: exec +UPDATE events +SET decision_release = $2 +WHERE id = $1; + +-- name: UpdateEventStartTime: exec +UPDATE events +SET start_time = $2 +WHERE id = $1; + +-- name: UpdateEventEndTime: exec +UPDATE events +SET end_time = $2 +WHERE id = $1; + +-- name: UpdateEventIsPublished: exec +UPDATE events +SET is_published = $2 +WHERE id = $1; + +-- name: UpdateEventWebsiteUrl: exec +UPDATE events +SET website_url = $2 +WHERE id = $1; + +-- name: DeleteEvent: exec +DELETE FROM events +WHERE id = $1; From bc1bd4fa4d4e5327374e575b04c791a0d2ea01de Mon Sep 17 00:00:00 2001 From: h1divp <71522316+h1divp@users.noreply.github.com> Date: Tue, 1 Jul 2025 21:50:02 -0400 Subject: [PATCH 05/52] removed newline --- apps/api/internal/db/migrations/20250619161938_event_schema.sql | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/api/internal/db/migrations/20250619161938_event_schema.sql b/apps/api/internal/db/migrations/20250619161938_event_schema.sql index ddb8929a..f8a5b4d2 100644 --- a/apps/api/internal/db/migrations/20250619161938_event_schema.sql +++ b/apps/api/internal/db/migrations/20250619161938_event_schema.sql @@ -13,7 +13,6 @@ CREATE TABLE events ( application_close TIMESTAMPTZ NOT NULL, rsvp_deadline TIMESTAMPTZ, decision_release TIMESTAMPTZ, - -- Event phase start_time TIMESTAMPTZ NOT NULL, From 543c619e7bd91b25dc95de1e05d26373650d5eaa Mon Sep 17 00:00:00 2001 From: Hieu Nguyen <76720778+hieunguyent12@users.noreply.github.com> Date: Mon, 30 Jun 2025 21:16:06 -0500 Subject: [PATCH 06/52] feat: added generic badge component, event badge component, and icon wrapper (#39) * feat: added generic badge component, event badge component, and icon wrapper * fix: updated darkmode colors and fixed incorrect icon colors * fix: small tweaks over rendering icons and eslint mutes * feat: added unplugin-icons and replaced old icons * fix: updated badge border property and storybook badge menu --------- Co-authored-by: AlexanderWangY --- apps/web/package.json | 5 + apps/web/pnpm-lock.yaml | 420 ++++++++++++++++++ apps/web/src/components/icons/IconWrapper.tsx | 29 ++ apps/web/src/components/icons/createIcon.tsx | 11 + .../src/components/ui/Badge/Badge.stories.tsx | 50 +++ .../src/components/ui/Badge/Badge.test.tsx | 38 ++ apps/web/src/components/ui/Badge/Badge.tsx | 46 ++ apps/web/src/components/ui/Badge/index.tsx | 3 + .../src/features/Auth/components/Login.tsx | 4 +- .../src/features/Event/applicationStatus.ts | 77 ++++ .../features/Event/components/EventBadge.tsx | 70 +++ .../components/stories/EventBadge.stories.tsx | 101 +++++ apps/web/src/theme.css | 72 +++ apps/web/tsconfig.app.json | 1 + apps/web/vite.config.ts | 11 +- 15 files changed, 933 insertions(+), 5 deletions(-) create mode 100644 apps/web/src/components/icons/IconWrapper.tsx create mode 100644 apps/web/src/components/icons/createIcon.tsx create mode 100644 apps/web/src/components/ui/Badge/Badge.stories.tsx create mode 100644 apps/web/src/components/ui/Badge/Badge.test.tsx create mode 100644 apps/web/src/components/ui/Badge/Badge.tsx create mode 100644 apps/web/src/components/ui/Badge/index.tsx create mode 100644 apps/web/src/features/Event/applicationStatus.ts create mode 100644 apps/web/src/features/Event/components/EventBadge.tsx create mode 100644 apps/web/src/features/Event/components/stories/EventBadge.stories.tsx diff --git a/apps/web/package.json b/apps/web/package.json index d667e5d3..0a04b0ab 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -44,6 +44,8 @@ "devDependencies": { "@chromatic-com/storybook": "^3", "@eslint/js": "^9.25.0", + "@iconify-json/ic": "^1.2.2", + "@iconify-json/tabler": "^1.2.19", "@storybook/addon-essentials": "^8.6.12", "@storybook/addon-onboarding": "^8.6.12", "@storybook/addon-styling-webpack": "^1.0.1", @@ -52,6 +54,8 @@ "@storybook/react": "^8.6.12", "@storybook/react-vite": "^8.6.12", "@storybook/test": "^8.6.12", + "@svgr/core": "^8.1.0", + "@svgr/plugin-jsx": "^8.1.0", "@tanstack/router-plugin": "^1.120.2", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.3.0", @@ -75,6 +79,7 @@ "storybook": "^8.6.12", "typescript": "~5.8.3", "typescript-eslint": "^8.30.1", + "unplugin-icons": "^22.1.0", "vite": "^6.3.5", "vitest": "^3.1.3" }, diff --git a/apps/web/pnpm-lock.yaml b/apps/web/pnpm-lock.yaml index 3067afe9..0b2af606 100644 --- a/apps/web/pnpm-lock.yaml +++ b/apps/web/pnpm-lock.yaml @@ -60,6 +60,12 @@ importers: '@eslint/js': specifier: ^9.25.0 version: 9.28.0 + '@iconify-json/ic': + specifier: ^1.2.2 + version: 1.2.2 + '@iconify-json/tabler': + specifier: ^1.2.19 + version: 1.2.19 '@storybook/addon-essentials': specifier: ^8.6.12 version: 8.6.14(@types/react@19.1.7)(storybook@8.6.14(prettier@3.5.3)) @@ -84,6 +90,12 @@ importers: '@storybook/test': specifier: ^8.6.12 version: 8.6.14(storybook@8.6.14(prettier@3.5.3)) + '@svgr/core': + specifier: ^8.1.0 + version: 8.1.0(typescript@5.8.3) + '@svgr/plugin-jsx': + specifier: ^8.1.0 + version: 8.1.0(@svgr/core@8.1.0(typescript@5.8.3)) '@tanstack/router-plugin': specifier: ^1.120.2 version: 1.121.0(@tanstack/react-router@1.121.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(vite@6.3.5(@types/node@22.15.31)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0))(webpack@5.99.9(esbuild@0.25.5)) @@ -153,6 +165,9 @@ importers: typescript-eslint: specifier: ^8.30.1 version: 8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) + unplugin-icons: + specifier: ^22.1.0 + 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.31)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0) @@ -169,6 +184,12 @@ packages: resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} + '@antfu/install-pkg@1.1.0': + resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} + + '@antfu/utils@8.1.1': + resolution: {integrity: sha512-Mex9nXf9vR6AhcXmMrlz/HVgYYZpVGJ6YlPgwl7UnaFpnshXs6EK/oa5Gpf3CzENMjkvEx2tQtntGnb7UtSTOQ==} + '@asamuzakjp/css-color@3.2.0': resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} @@ -574,6 +595,18 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} + '@iconify-json/ic@1.2.2': + resolution: {integrity: sha512-QmjwS3lYiOmVWgTCEOTFyGODaR/+689+ajep/VsrCcsUN0Gdle5PmIcibDsdmRyrOsW/E77G41UUijdbjQUofw==} + + '@iconify-json/tabler@1.2.19': + resolution: {integrity: sha512-JDeQTQxHD8KE12pAbPVHX1WFVOPq8D0XfRb/LwYHwGwYE0HP9OIjJ//TKxS1Gt++RirYu6Xsx+Jm5LA5KbykoA==} + + '@iconify/types@2.0.0': + resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} + + '@iconify/utils@2.3.0': + resolution: {integrity: sha512-GmQ78prtwYW6EtzXRU1rY+KwOKfz32PD7iJh6Iyqw68GiKuoZ2A6pRtzWONz5VQJbp50mEjXh/7NkumtrAgRKA==} + '@internationalized/date@3.8.2': resolution: {integrity: sha512-/wENk7CbvLbkUvX1tu0mwq49CVkkWpkXubGel6birjRPyo6uQ4nQpnq5xZu823zRCwwn82zgHrvgF1vZyvmVgA==} @@ -1540,6 +1573,74 @@ packages: peerDependencies: storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 + '@svgr/babel-plugin-add-jsx-attribute@8.0.0': + resolution: {integrity: sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@svgr/babel-plugin-remove-jsx-attribute@8.0.0': + resolution: {integrity: sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0': + resolution: {integrity: sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0': + resolution: {integrity: sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@svgr/babel-plugin-svg-dynamic-title@8.0.0': + resolution: {integrity: sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@svgr/babel-plugin-svg-em-dimensions@8.0.0': + resolution: {integrity: sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@svgr/babel-plugin-transform-react-native-svg@8.1.0': + resolution: {integrity: sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@svgr/babel-plugin-transform-svg-component@8.0.0': + resolution: {integrity: sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw==} + engines: {node: '>=12'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@svgr/babel-preset@8.1.0': + resolution: {integrity: sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@svgr/core@8.1.0': + resolution: {integrity: sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==} + engines: {node: '>=14'} + + '@svgr/hast-util-to-babel-ast@8.0.0': + resolution: {integrity: sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q==} + engines: {node: '>=14'} + + '@svgr/plugin-jsx@8.1.0': + resolution: {integrity: sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA==} + engines: {node: '>=14'} + peerDependencies: + '@svgr/core': '*' + '@swc/helpers@0.5.17': resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==} @@ -2137,6 +2238,10 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} + camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + caniuse-lite@1.0.30001721: resolution: {integrity: sha512-cOuvmUVtKrtEaoKiO0rSc29jcjwMwX5tOHDy4MgVFEWiUXj4uBMJkwI8MDySkgXidpMiHUcviogAvFi4pA2hDQ==} @@ -2223,9 +2328,24 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + confbox@0.2.2: + resolution: {integrity: sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==} + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cosmiconfig@8.3.6: + resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -2297,6 +2417,9 @@ packages: dom-accessibility-api@0.6.3: resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + dot-case@3.0.4: + resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -2320,6 +2443,10 @@ packages: resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==} engines: {node: '>=10.13.0'} + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + entities@6.0.1: resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} engines: {node: '>=0.12'} @@ -2328,6 +2455,9 @@ packages: resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} engines: {node: '>=18'} + error-ex@1.3.2: + resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + es-define-property@1.0.1: resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} engines: {node: '>= 0.4'} @@ -2458,6 +2588,9 @@ packages: resolution: {integrity: sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==} engines: {node: '>=12.0.0'} + exsolve@1.0.7: + resolution: {integrity: sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -2588,6 +2721,10 @@ packages: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} + globals@15.15.0: + resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==} + engines: {node: '>=18'} + globals@16.2.0: resolution: {integrity: sha512-O+7l9tPdHCU320IigZZPj5zmRCFG9xHmx9cU8FqU2Rp+JN714seHV+2S9+JslCpY4gJwU2vOGox0wzgae/MCEg==} engines: {node: '>=18'} @@ -2679,6 +2816,9 @@ packages: resolution: {integrity: sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==} engines: {node: '>= 0.4'} + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + is-binary-path@2.1.0: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} @@ -2835,6 +2975,9 @@ packages: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} + kolorist@1.8.0: + resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} + ky@1.8.1: resolution: {integrity: sha512-7Bp3TpsE+L+TARSnnDpk3xg8Idi8RwSLdj6CMbNWoOARIrGrbuLGusV0dYwbZOm4bB3jHNxSw8Wk/ByDqJEnDw==} engines: {node: '>=18'} @@ -2911,6 +3054,9 @@ packages: resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} engines: {node: '>=14'} + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + lint-staged@15.5.2: resolution: {integrity: sha512-YUSOLq9VeRNAo/CTaVmhGDKG+LBtA8KF1X4K5+ykMSwWST1vDxJRB2kv2COgLb1fvpCo+A/y9A0G0znNVmdx4w==} engines: {node: '>=18.12.0'} @@ -2924,6 +3070,10 @@ packages: resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==} engines: {node: '>=6.11.5'} + local-pkg@1.1.1: + resolution: {integrity: sha512-WunYko2W1NcdfAFpuLUoucsgULmgDBRkdxHxWQ7mK0cQqwPiy8E1enjuRBrhLtZkB5iScJ1XIPdhVEFK8aOLSg==} + engines: {node: '>=14'} + locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} @@ -2941,6 +3091,9 @@ packages: loupe@3.1.3: resolution: {integrity: sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==} + lower-case@2.0.2: + resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} + lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -3029,6 +3182,9 @@ packages: engines: {node: '>=10'} hasBin: true + mlly@1.7.4: + resolution: {integrity: sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==} + mrmime@2.0.1: resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} engines: {node: '>=10'} @@ -3047,6 +3203,9 @@ packages: neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + no-case@3.0.4: + resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} + node-releases@2.0.19: resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} @@ -3088,10 +3247,17 @@ packages: package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + package-manager-detector@1.3.0: + resolution: {integrity: sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ==} + parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + parse5@7.3.0: resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} @@ -3114,6 +3280,10 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -3137,6 +3307,12 @@ packages: engines: {node: '>=0.10'} hasBin: true + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + + pkg-types@2.1.1: + resolution: {integrity: sha512-eY0QFb6eSwc9+0d/5D2lFFUq+A3n3QNGSy/X2Nvp+6MfzGw2u6EbA7S80actgjY1lkvvI0pqB+a4hioMh443Ew==} + playwright-core@1.53.0: resolution: {integrity: sha512-mGLg8m0pm4+mmtB7M89Xw/GSqoNC+twivl8ITteqvAndachozYe2ZA7srU6uleV1vEdAHYqjq+SV8SNxRRFYBw==} engines: {node: '>=18'} @@ -3187,6 +3363,9 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + quansync@0.2.10: + resolution: {integrity: sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A==} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -3358,6 +3537,9 @@ packages: resolution: {integrity: sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==} engines: {node: '>=18'} + snake-case@3.0.4: + resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -3447,6 +3629,9 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + svg-parser@2.0.4: + resolution: {integrity: sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==} + symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} @@ -3515,6 +3700,9 @@ packages: tinyexec@0.3.2: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + tinyexec@1.0.1: + resolution: {integrity: sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==} + tinyglobby@0.2.14: resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} engines: {node: '>=12.0.0'} @@ -3607,6 +3795,9 @@ packages: engines: {node: '>=14.17'} hasBin: true + ufo@1.6.1: + resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} + undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} @@ -3614,6 +3805,29 @@ packages: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} + unplugin-icons@22.1.0: + resolution: {integrity: sha512-ect2ZNtk1Zgwb0NVHd0C1IDW/MV+Jk/xaq4t8o6rYdVS3+L660ZdD5kTSQZvsgdwCvquRw+/wYn75hsweRjoIA==} + peerDependencies: + '@svgr/core': '>=7.0.0' + '@svgx/core': ^1.0.1 + '@vue/compiler-sfc': ^3.0.2 || ^2.7.0 + svelte: ^3.0.0 || ^4.0.0 || ^5.0.0 + vue-template-compiler: ^2.6.12 + vue-template-es2015-compiler: ^1.9.0 + peerDependenciesMeta: + '@svgr/core': + optional: true + '@svgx/core': + optional: true + '@vue/compiler-sfc': + optional: true + svelte: + optional: true + vue-template-compiler: + optional: true + vue-template-es2015-compiler: + optional: true + unplugin@1.16.1: resolution: {integrity: sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w==} engines: {node: '>=14.0.0'} @@ -3834,6 +4048,13 @@ snapshots: '@jridgewell/gen-mapping': 0.3.8 '@jridgewell/trace-mapping': 0.3.25 + '@antfu/install-pkg@1.1.0': + dependencies: + package-manager-detector: 1.3.0 + tinyexec: 1.0.1 + + '@antfu/utils@8.1.1': {} + '@asamuzakjp/css-color@3.2.0': dependencies: '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) @@ -4231,6 +4452,29 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} + '@iconify-json/ic@1.2.2': + dependencies: + '@iconify/types': 2.0.0 + + '@iconify-json/tabler@1.2.19': + dependencies: + '@iconify/types': 2.0.0 + + '@iconify/types@2.0.0': {} + + '@iconify/utils@2.3.0': + dependencies: + '@antfu/install-pkg': 1.1.0 + '@antfu/utils': 8.1.1 + '@iconify/types': 2.0.0 + debug: 4.4.1 + globals: 15.15.0 + kolorist: 1.8.0 + local-pkg: 1.1.1 + mlly: 1.7.4 + transitivePeerDependencies: + - supports-color + '@internationalized/date@3.8.2': dependencies: '@swc/helpers': 0.5.17 @@ -5671,6 +5915,76 @@ snapshots: dependencies: storybook: 8.6.14(prettier@3.5.3) + '@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + + '@svgr/babel-plugin-remove-jsx-attribute@8.0.0(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + + '@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + + '@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + + '@svgr/babel-plugin-svg-dynamic-title@8.0.0(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + + '@svgr/babel-plugin-svg-em-dimensions@8.0.0(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + + '@svgr/babel-plugin-transform-react-native-svg@8.1.0(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + + '@svgr/babel-plugin-transform-svg-component@8.0.0(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + + '@svgr/babel-preset@8.1.0(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@svgr/babel-plugin-add-jsx-attribute': 8.0.0(@babel/core@7.27.4) + '@svgr/babel-plugin-remove-jsx-attribute': 8.0.0(@babel/core@7.27.4) + '@svgr/babel-plugin-remove-jsx-empty-expression': 8.0.0(@babel/core@7.27.4) + '@svgr/babel-plugin-replace-jsx-attribute-value': 8.0.0(@babel/core@7.27.4) + '@svgr/babel-plugin-svg-dynamic-title': 8.0.0(@babel/core@7.27.4) + '@svgr/babel-plugin-svg-em-dimensions': 8.0.0(@babel/core@7.27.4) + '@svgr/babel-plugin-transform-react-native-svg': 8.1.0(@babel/core@7.27.4) + '@svgr/babel-plugin-transform-svg-component': 8.0.0(@babel/core@7.27.4) + + '@svgr/core@8.1.0(typescript@5.8.3)': + dependencies: + '@babel/core': 7.27.4 + '@svgr/babel-preset': 8.1.0(@babel/core@7.27.4) + camelcase: 6.3.0 + cosmiconfig: 8.3.6(typescript@5.8.3) + snake-case: 3.0.4 + transitivePeerDependencies: + - supports-color + - typescript + + '@svgr/hast-util-to-babel-ast@8.0.0': + dependencies: + '@babel/types': 7.27.6 + entities: 4.5.0 + + '@svgr/plugin-jsx@8.1.0(@svgr/core@8.1.0(typescript@5.8.3))': + dependencies: + '@babel/core': 7.27.4 + '@svgr/babel-preset': 8.1.0(@babel/core@7.27.4) + '@svgr/core': 8.1.0(typescript@5.8.3) + '@svgr/hast-util-to-babel-ast': 8.0.0 + svg-parser: 2.0.4 + transitivePeerDependencies: + - supports-color + '@swc/helpers@0.5.17': dependencies: tslib: 2.8.1 @@ -6394,6 +6708,8 @@ snapshots: callsites@3.1.0: {} + camelcase@6.3.0: {} + caniuse-lite@1.0.30001721: {} chai@5.2.0: @@ -6467,8 +6783,21 @@ snapshots: concat-map@0.0.1: {} + confbox@0.1.8: {} + + confbox@0.2.2: {} + convert-source-map@2.0.0: {} + cosmiconfig@8.3.6(typescript@5.8.3): + dependencies: + import-fresh: 3.3.1 + js-yaml: 4.1.0 + parse-json: 5.2.0 + path-type: 4.0.0 + optionalDependencies: + typescript: 5.8.3 + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -6523,6 +6852,11 @@ snapshots: dom-accessibility-api@0.6.3: {} + dot-case@3.0.4: + dependencies: + no-case: 3.0.4 + tslib: 2.8.1 + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -6544,10 +6878,16 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.2.2 + entities@4.5.0: {} + entities@6.0.1: {} environment@1.1.0: {} + error-ex@1.3.2: + dependencies: + is-arrayish: 0.2.1 + es-define-property@1.0.1: {} es-errors@1.3.0: {} @@ -6724,6 +7064,8 @@ snapshots: expect-type@1.2.1: {} + exsolve@1.0.7: {} + fast-deep-equal@3.1.3: {} fast-glob@3.3.3: @@ -6848,6 +7190,8 @@ snapshots: globals@14.0.0: {} + globals@15.15.0: {} + globals@16.2.0: {} gopd@1.2.0: {} @@ -6927,6 +7271,8 @@ snapshots: call-bound: 1.0.4 has-tostringtag: 1.0.2 + is-arrayish@0.2.1: {} + is-binary-path@2.1.0: dependencies: binary-extensions: 2.3.0 @@ -7083,6 +7429,8 @@ snapshots: kleur@3.0.3: {} + kolorist@1.8.0: {} + ky@1.8.1: {} levn@0.4.1: @@ -7137,6 +7485,8 @@ snapshots: lilconfig@3.1.3: {} + lines-and-columns@1.2.4: {} + lint-staged@15.5.2: dependencies: chalk: 5.4.1 @@ -7163,6 +7513,12 @@ snapshots: loader-runner@4.3.0: {} + local-pkg@1.1.1: + dependencies: + mlly: 1.7.4 + pkg-types: 2.1.1 + quansync: 0.2.10 + locate-path@6.0.0: dependencies: p-locate: 5.0.0 @@ -7181,6 +7537,10 @@ snapshots: loupe@3.1.3: {} + lower-case@2.0.2: + dependencies: + tslib: 2.8.1 + lru-cache@10.4.3: {} lru-cache@5.1.1: @@ -7254,6 +7614,13 @@ snapshots: mkdirp@3.0.1: {} + mlly@1.7.4: + dependencies: + acorn: 8.15.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.1 + mrmime@2.0.1: {} ms@2.1.3: {} @@ -7264,6 +7631,11 @@ snapshots: neo-async@2.6.2: {} + no-case@3.0.4: + dependencies: + lower-case: 2.0.2 + tslib: 2.8.1 + node-releases@2.0.19: {} normalize-path@3.0.0: {} @@ -7307,10 +7679,19 @@ snapshots: package-json-from-dist@1.0.1: {} + package-manager-detector@1.3.0: {} + parent-module@1.0.1: dependencies: callsites: 3.1.0 + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.27.1 + error-ex: 1.3.2 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + parse5@7.3.0: dependencies: entities: 6.0.1 @@ -7328,6 +7709,8 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.2 + path-type@4.0.0: {} + pathe@2.0.3: {} pathval@2.0.0: {} @@ -7340,6 +7723,18 @@ snapshots: pidtree@0.6.0: {} + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.7.4 + pathe: 2.0.3 + + pkg-types@2.1.1: + dependencies: + confbox: 0.2.2 + exsolve: 1.0.7 + pathe: 2.0.3 + playwright-core@1.53.0: {} playwright@1.53.0: @@ -7381,6 +7776,8 @@ snapshots: punycode@2.3.1: {} + quansync@0.2.10: {} + queue-microtask@1.2.3: {} randombytes@2.1.0: @@ -7670,6 +8067,11 @@ snapshots: ansi-styles: 6.2.1 is-fullwidth-code-point: 5.0.0 + snake-case@3.0.4: + dependencies: + dot-case: 3.0.4 + tslib: 2.8.1 + source-map-js@1.2.1: {} source-map-support@0.5.21: @@ -7751,6 +8153,8 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + svg-parser@2.0.4: {} + symbol-tree@3.2.4: {} tailwind-merge@3.0.2: {} @@ -7811,6 +8215,8 @@ snapshots: tinyexec@0.3.2: {} + tinyexec@1.0.1: {} + tinyglobby@0.2.14: dependencies: fdir: 6.4.6(picomatch@4.0.2) @@ -7887,10 +8293,24 @@ snapshots: typescript@5.8.3: {} + ufo@1.6.1: {} + undici-types@6.21.0: {} universalify@2.0.1: {} + unplugin-icons@22.1.0(@svgr/core@8.1.0(typescript@5.8.3)): + dependencies: + '@antfu/install-pkg': 1.1.0 + '@iconify/utils': 2.3.0 + debug: 4.4.1 + local-pkg: 1.1.1 + unplugin: 2.3.5 + optionalDependencies: + '@svgr/core': 8.1.0(typescript@5.8.3) + transitivePeerDependencies: + - supports-color + unplugin@1.16.1: dependencies: acorn: 8.15.0 diff --git a/apps/web/src/components/icons/IconWrapper.tsx b/apps/web/src/components/icons/IconWrapper.tsx new file mode 100644 index 00000000..f1725544 --- /dev/null +++ b/apps/web/src/components/icons/IconWrapper.tsx @@ -0,0 +1,29 @@ +import { cn } from "@/utils/cn"; +import type { JSX } from "react"; +import React from "react"; + +export type IconWrapperProps = React.HTMLAttributes & { + children: JSX.Element; +}; + +// Simplified version of https://www.jacobparis.com/content/react-as-child +// This wrapper is used to wrap any SVG icon which allows us to inject additional props to those icons (like className) +// without requiring us to define those props on the icons themselves which reduces code duplication. +const IconWrapper = ({ children, ...props }: IconWrapperProps) => { + if (!React.isValidElement(children)) { + throw new Error("Invalid icon component passed to IconWrapper."); + } + + if (children.type !== "svg") { + throw new Error( + `Icon must be an svg element. The erroneous icon has type \`${children.type}\``, + ); + } + + return React.cloneElement(children, { + ...props, + className: cn(children.props.className, props.className), // custom props will override the icon's + }); +}; + +export { IconWrapper }; diff --git a/apps/web/src/components/icons/createIcon.tsx b/apps/web/src/components/icons/createIcon.tsx new file mode 100644 index 00000000..f3f76128 --- /dev/null +++ b/apps/web/src/components/icons/createIcon.tsx @@ -0,0 +1,11 @@ +import type { JSX } from "react"; +import { IconWrapper, type IconWrapperProps } from "./IconWrapper"; + +// All SVG icons must call this function. +// It allows us to do something like without requiring us to do define and inject +// props inside the Icon directly. +export const createIcon = (iconSvg: () => JSX.Element) => { + return (props?: Omit) => ( + {iconSvg()} + ); +}; diff --git a/apps/web/src/components/ui/Badge/Badge.stories.tsx b/apps/web/src/components/ui/Badge/Badge.stories.tsx new file mode 100644 index 00000000..f36181eb --- /dev/null +++ b/apps/web/src/components/ui/Badge/Badge.stories.tsx @@ -0,0 +1,50 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { Badge } from "."; + +const meta = { + component: Badge, + title: "UI/Badge", + tags: ["autodocs"], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + children: "Badge", + type: "default", + size: "sm", + }, +}; + +export const DefaultWithIcon: Story = { + args: { + children: ( + <> + + + + Badge + + ), + size: "sm", + }, +}; + +export const DefaultMediumSize: Story = { + args: { + children: <>Badge, + size: "md", + }, +}; diff --git a/apps/web/src/components/ui/Badge/Badge.test.tsx b/apps/web/src/components/ui/Badge/Badge.test.tsx new file mode 100644 index 00000000..f367c79b --- /dev/null +++ b/apps/web/src/components/ui/Badge/Badge.test.tsx @@ -0,0 +1,38 @@ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import { Badge } from "."; + +describe("Badge component", () => { + it("renders with correct text", () => { + render(A Badge!); + const badge = screen.getByText(/A Badge!/i); + expect(badge).toBeInTheDocument(); + }); + + it("renders with correct icon and text", () => { + render( + + + + + A Badge! + , + ); + let badge = screen.getByTestId("badge-icon"); + expect(badge).toBeInTheDocument(); + + badge = screen.getByText(/A Badge!/i); + expect(badge).toBeInTheDocument(); + }); +}); diff --git a/apps/web/src/components/ui/Badge/Badge.tsx b/apps/web/src/components/ui/Badge/Badge.tsx new file mode 100644 index 00000000..a2270575 --- /dev/null +++ b/apps/web/src/components/ui/Badge/Badge.tsx @@ -0,0 +1,46 @@ +/* eslint-disable react-refresh/only-export-components */ +import { forwardRef } from "react"; +import { tv, type VariantProps } from "tailwind-variants"; + +export const badge = tv({ + base: "inline-flex items-center gap-1 rounded-xl font-medium select-none", + variants: { + type: { + default: "bg-badge-bg-default text-badge-text-default", + }, + size: { + sm: "px-2 py-1 text-xs", + md: "px-2.5 py-1.5 text-sm", + }, + border: { + sm: "rounded-sm", + md: "rounded-md", + lg: "rounded-lg", + xl: "rounded-xl", + }, + }, + + defaultVariants: { + size: "sm", + type: "default", + border: "xl", + }, +}); + +type BadgeVariants = VariantProps; + +export interface BadgeProps + extends BadgeVariants, + React.HTMLAttributes {} + +const Badge = forwardRef( + ({ size, type, border, className, ...props }, ref) => { + const badgeClassName = badge({ size, type, className, border }); + + return ; + }, +); + +Badge.displayName = "Badge"; + +export { Badge }; diff --git a/apps/web/src/components/ui/Badge/index.tsx b/apps/web/src/components/ui/Badge/index.tsx new file mode 100644 index 00000000..92727d7b --- /dev/null +++ b/apps/web/src/components/ui/Badge/index.tsx @@ -0,0 +1,3 @@ +/* eslint-disable react-refresh/only-export-components */ +export { Badge, badge } from "./Badge"; +export type { BadgeProps } from "./Badge"; diff --git a/apps/web/src/features/Auth/components/Login.tsx b/apps/web/src/features/Auth/components/Login.tsx index 0add5940..59013685 100644 --- a/apps/web/src/features/Auth/components/Login.tsx +++ b/apps/web/src/features/Auth/components/Login.tsx @@ -1,6 +1,6 @@ -import { DiscordIcon } from "@/components/icons/Discord"; import { Button } from "@/components/ui/Button"; import { useTheme } from "@/components/ThemeProvider"; +import IcBaselineDiscord from "~icons/ic/baseline-discord"; import { useSearch } from "@tanstack/react-router"; import { auth } from "@/lib/authClient"; @@ -32,7 +32,7 @@ const Login = () => { onClick={() => auth.oauth.signIn("discord", redirect)} > - + Log in with Discord diff --git a/apps/web/src/features/Event/applicationStatus.ts b/apps/web/src/features/Event/applicationStatus.ts new file mode 100644 index 00000000..e505bbac --- /dev/null +++ b/apps/web/src/features/Event/applicationStatus.ts @@ -0,0 +1,77 @@ +import type { ComponentType } from "react"; +import TablerBan from "~icons/tabler/ban"; +import TablerUserCheck from "~icons/tabler/user-check"; +import TablerConfetti from "~icons/tabler/confetti"; +import TablerClockPause from "~icons/tabler/clock-pause"; +import TablerHourglassFilled from "~icons/tabler/hourglass-filled"; +import TablerPointFilled from "~icons/tabler/point-filled"; +import TablerId from "~icons/tabler/id"; +import TablerSettings2 from "~icons/tabler/settings-2"; +import TablerCalendarCheck from "~icons/tabler/calendar-check"; + +type ApplicationStatus = { + [k: string]: { + className: string; + text: string; + icon?: ComponentType>; + }; +}; + +const defineStatus = (status: T) => { + return status; +}; + +const applicationStatus = defineStatus({ + rejected: { + className: "bg-badge-bg-rejected text-badge-text-rejected", + text: "Rejected", + icon: TablerBan, + }, + attending: { + className: "bg-badge-bg-attending text-badge-text-attending", + text: "Attending", + icon: TablerUserCheck, + }, + accepted: { + className: "bg-badge-bg-accepted text-badge-text-accepted", + text: "Accepted", + icon: TablerConfetti, + }, + waitlisted: { + className: "bg-badge-bg-waitlisted text-badge-text-waitlisted", + text: "Waitlisted", + icon: TablerClockPause, + }, + underReview: { + className: "bg-badge-bg-underReview text-badge-text-underReview", + text: "Under Review", + icon: TablerHourglassFilled, + }, + notApplied: { + className: "bg-badge-bg-notApplied text-badge-text-notApplied", + text: "Not Applied", + icon: TablerPointFilled, + }, + staff: { + className: "bg-badge-bg-staff text-badge-text-staff", + text: "Staff", + icon: TablerId, + }, + admin: { + className: "bg-badge-bg-admin text-badge-text-admin", + text: "Admin", + icon: TablerSettings2, + }, + notGoing: { + className: "bg-badge-bg-notGoing text-badge-text-notGoing", + text: "Not Going", + icon: TablerBan, + }, + completed: { + className: "bg-badge-bg-completed text-badge-text-completed", + text: "Completed", + icon: TablerCalendarCheck, + }, +}); + +export default applicationStatus; diff --git a/apps/web/src/features/Event/components/EventBadge.tsx b/apps/web/src/features/Event/components/EventBadge.tsx new file mode 100644 index 00000000..7f887f14 --- /dev/null +++ b/apps/web/src/features/Event/components/EventBadge.tsx @@ -0,0 +1,70 @@ +import { Badge, badge, type BadgeProps } from "@/components/ui/Badge"; +import { tv } from "tailwind-variants"; +import applicationStatus from "../applicationStatus"; + +type ApplicationStatusTypes = keyof typeof applicationStatus; + +/* + * Transforms the `applicationStatus` object into a flattened variant mapping + * where each status key maps to its corresponding className. + * + * For example, it converts: + * + * { + * rejected: { className: "bg-badge-bg-rejected text-badge-text-rejected", ... }, + * } + * + * to: + * + * { + * rejected: "bg-badge-bg-rejected text-badge-text-rejected", + * } + */ +const applicationStatusVariants = Object.fromEntries( + Object.entries(applicationStatus).map(([key, value]) => [ + key, + value.className, + ]), +) as { + [K in ApplicationStatusTypes]: (typeof applicationStatus)[K]["className"]; +}; + +export const eventBadge = tv({ + extend: badge, + variants: { + type: {}, // override the `type` variant in the Badge component + status: applicationStatusVariants, + }, +}); + +interface EventBadgeProps extends Omit { + status: ApplicationStatusTypes; +} + +const EventBadge = ({ status: statusProp, size, border }: EventBadgeProps) => { + const eventBadgeClassname = eventBadge({ status: statusProp, size, border }); + const status = applicationStatus[statusProp]; + const BadgeIcon = status?.icon; + + if (!status) { + console.error( + `Incorrect status prop passed to EventBadge component: ${statusProp}`, + ); + return null; + } + + return ( + + {BadgeIcon && } + + {status.text} + + + ); +}; + +export { EventBadge }; diff --git a/apps/web/src/features/Event/components/stories/EventBadge.stories.tsx b/apps/web/src/features/Event/components/stories/EventBadge.stories.tsx new file mode 100644 index 00000000..afe3858b --- /dev/null +++ b/apps/web/src/features/Event/components/stories/EventBadge.stories.tsx @@ -0,0 +1,101 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { EventBadge, eventBadge } from "../EventBadge"; + +const entries = []; + +for (let i = 0; i < eventBadge.variantKeys.length; i++) { + const key = eventBadge.variantKeys[i]; + + if (key === "type") continue; + + entries.push([ + key, + { + control: { + type: "select", + }, + options: Object.keys(eventBadge.variants[key]), + }, + ]); +} +const argTypes = Object.fromEntries(entries); + +const meta = { + component: EventBadge, + title: "UI/Event Badge", + tags: ["autodocs"], + argTypes, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Rejected: Story = { + args: { + status: "rejected", + size: "sm", + }, +}; + +export const Attending: Story = { + args: { + status: "attending", + size: "sm", + }, +}; + +export const Accepted: Story = { + args: { + status: "accepted", + size: "sm", + }, +}; + +export const Waitlisted: Story = { + args: { + status: "waitlisted", + size: "sm", + }, +}; + +export const UnderReview: Story = { + args: { + status: "underReview", + size: "sm", + }, +}; + +export const NotApplied: Story = { + args: { + status: "notApplied", + size: "sm", + }, +}; + +export const Staff: Story = { + args: { + status: "staff", + size: "sm", + }, +}; + +export const Admin: Story = { + args: { + status: "admin", + size: "sm", + }, +}; + +export const NotGoing: Story = { + args: { + status: "notGoing", + size: "sm", + }, +}; + +export const Completed: Story = { + args: { + status: "completed", + size: "sm", + }, +}; diff --git a/apps/web/src/theme.css b/apps/web/src/theme.css index eb08edb0..4beddc9a 100644 --- a/apps/web/src/theme.css +++ b/apps/web/src/theme.css @@ -15,6 +15,30 @@ --button-outline: oklch(0.6187 0.2067 259.23 / 25%); --button-outline-hover: oklch(0.5465 0.2455 262.87 / 25%); --button-outline-disabled: oklch(0.8071 0.10065 250.4462 / 25%); + + --badge-bg-default: var(--color-neutral-200); + --badge-bg-rejected: var(--color-red-200); + --badge-bg-attending: var(--color-blue-200); + --badge-bg-accepted: var(--color-green-200); + --badge-bg-waitlisted: var(--color-violet-200); + --badge-bg-underReview: var(--color-orange-200); + --badge-bg-notApplied: var(--color-neutral-300); + --badge-bg-staff: var(--color-sky-200); + --badge-bg-admin: var(--color-fuchsia-200); + --badge-bg-notGoing: var(--color-red-200); + --badge-bg-completed: var(--color-indigo-200); + + --badge-text-default: var(--color-neutral-600); + --badge-text-attending: var(--color-blue-600); + --badge-text-rejected: var(--color-red-600); + --badge-text-accepted: var(--color-green-600); + --badge-text-waitlisted: var(--color-violet-600); + --badge-text-underReview: var(--color-orange-600); + --badge-text-notApplied: var(--color-stone-600); + --badge-text-staff: var(--color-sky-600); + --badge-text-admin: var(--color-fuchsia-600); + --badge-text-notGoing: var(--color-red-600); + --badge-text-completed: var(--color-indigo-600); } .dark { @@ -34,6 +58,30 @@ --button-outline: oklch(0.6187 0.2067 259.23 / 25%); --button-outline-hover: oklch(0.5465 0.2455 262.87 / 25%); --button-outline-disabled: oklch(0.8071 0.10065 250.4462 / 25%); + + --badge-bg-default: var(--color-neutral-200); + --badge-bg-rejected: oklch(0.3378 0.0595 20.68); + --badge-bg-attending: oklch(0.3078 0.0681 265.53); + --badge-bg-accepted: oklch(0.3701 0.0671 153.92); + --badge-bg-waitlisted: oklch(0.3838 0.082 292.43); + --badge-bg-underReview: oklch(0.3668 0.0531 65.94); + --badge-bg-notApplied: oklch(0.3715 0 0); + --badge-bg-staff: oklch(0.3339 0.0442 235.27); + --badge-bg-admin: oklch(0.3526 0.0681 318.27); + --badge-bg-notGoing: oklch(0.3378 0.0595 20.68); + --badge-bg-completed: var(--color-slate-700); + + --badge-text-default: var(--color-neutral-600); + --badge-text-attending: var(--color-blue-300); + --badge-text-rejected: var(--color-red-400); + --badge-text-accepted: var(--color-green-300); + --badge-text-waitlisted: oklch(0.9244 0.041 296.28); + --badge-text-underReview: var(--color-orange-300); + --badge-text-notApplied: var(--color-stone-300); + --badge-text-staff: var(--color-sky-200); + --badge-text-admin: var(--color-fuchsia-400); + --badge-text-notGoing: var(--color-red-400); + --badge-text-completed: var(--color-slate-300); } @theme inline { @@ -52,5 +100,29 @@ --color-button-outline-hover: var(--button-outline-hover); --color-button-outline-disabled: var(--button-outline-disabled); + --color-badge-bg-default: var(--badge-bg-default); + --color-badge-bg-rejected: var(--badge-bg-rejected); + --color-badge-bg-attending: var(--badge-bg-attending); + --color-badge-bg-accepted: var(--badge-bg-accepted); + --color-badge-bg-waitlisted: var(--badge-bg-waitlisted); + --color-badge-bg-underReview: var(--badge-bg-underReview); + --color-badge-bg-notApplied: var(--badge-bg-notApplied); + --color-badge-bg-staff: var(--badge-bg-staff); + --color-badge-bg-admin: var(--badge-bg-admin); + --color-badge-bg-notGoing: var(--badge-bg-notGoing); + --color-badge-bg-completed: var(--badge-bg-completed); + + --color-badge-text-default: var(--badge-text-default); + --color-badge-text-attending: var(--badge-text-attending); + --color-badge-text-rejected: var(--badge-text-rejected); + --color-badge-text-accepted: var(--badge-text-accepted); + --color-badge-text-waitlisted: var(--badge-text-waitlisted); + --color-badge-text-underReview: var(--badge-text-underReview); + --color-badge-text-notApplied: var(--badge-text-notApplied); + --color-badge-text-staff: var(--badge-text-staff); + --color-badge-text-admin: var(--badge-text-admin); + --color-badge-text-notGoing: var(--badge-text-notGoing); + --color-badge-text-completed: var(--badge-text-completed); + --font-figtree: "Figtree", sans-serif; } diff --git a/apps/web/tsconfig.app.json b/apps/web/tsconfig.app.json index caec2a82..1ee294f5 100644 --- a/apps/web/tsconfig.app.json +++ b/apps/web/tsconfig.app.json @@ -6,6 +6,7 @@ "lib": ["ES2024", "DOM", "DOM.Iterable"], "module": "ESNext", "skipLibCheck": true, + "types": ["unplugin-icons/types/react"], /* Bundler mode */ "moduleResolution": "bundler", diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index 6fa27de0..2915f873 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -4,9 +4,10 @@ import react from "@vitejs/plugin-react"; import tanstackRouter from "@tanstack/router-plugin/vite"; import tailwindcss from "@tailwindcss/vite"; import path from "path"; +import Icons from "unplugin-icons/vite"; // https://vitejs.dev/config/ -export default defineConfig({ +export default defineConfig(({ mode }) => ({ envPrefix: "VITE", plugins: [ tanstackRouter({ @@ -16,6 +17,10 @@ export default defineConfig({ }), react(), tailwindcss(), + Icons({ + compiler: "jsx", + jsx: "react", + }), ], test: { environment: "jsdom", @@ -28,6 +33,6 @@ export default defineConfig({ }, }, esbuild: { - drop: ["console", "debugger"], + drop: mode === "production" ? ["console", "debugger"] : [], }, -}); +})); From 1b877f1421f79bb52499215ff43fb70830638af3 Mon Sep 17 00:00:00 2001 From: Alexander Wang <98280966+AlexanderWangY@users.noreply.github.com> Date: Mon, 30 Jun 2025 22:42:45 -0400 Subject: [PATCH 07/52] fix: removed mounting empty node_modules in docker compose file (#41) --- .dockerignore | 1 + apps/web/Dockerfile.dev | 2 +- apps/web/pnpm-lock.yaml | 1382 ++++++++++++++++++++------------------- docker-compose.yml | 5 +- 4 files changed, 700 insertions(+), 690 deletions(-) diff --git a/.dockerignore b/.dockerignore index 4f75c951..65cf1c8c 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,4 +1,5 @@ node_modules +.vite .git .gitignore *.md diff --git a/apps/web/Dockerfile.dev b/apps/web/Dockerfile.dev index 78d5a542..7040e0cd 100644 --- a/apps/web/Dockerfile.dev +++ b/apps/web/Dockerfile.dev @@ -11,7 +11,7 @@ ENV PATH="$PNPM_HOME:$PATH" RUN npm install -g pnpm # Copy package files -COPY pnpm-lock.yaml package.json ./ +COPY package.json pnpm-lock.yaml ./ # Install dependencies using pnpm RUN pnpm install --frozen-lockfile diff --git a/apps/web/pnpm-lock.yaml b/apps/web/pnpm-lock.yaml index 0b2af606..095afabd 100644 --- a/apps/web/pnpm-lock.yaml +++ b/apps/web/pnpm-lock.yaml @@ -10,16 +10,16 @@ importers: dependencies: '@tailwindcss/vite': specifier: ^4.1.5 - version: 4.1.8(vite@6.3.5(@types/node@22.15.31)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(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.43.1)(tsx@4.20.3)(yaml@2.8.0)) '@tanstack/react-query': specifier: ^5.75.5 - version: 5.80.6(react@19.1.0) + version: 5.81.5(react@19.1.0) '@tanstack/react-router': specifier: ^1.120.2 - version: 1.121.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + version: 1.123.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) axios: specifier: ^1.9.0 - version: 1.9.0 + version: 1.10.0 clsx: specifier: ^2.1.1 version: 2.1.1 @@ -43,23 +43,23 @@ importers: version: 3.3.1 tailwind-variants: specifier: ^1.0.0 - version: 1.0.0(tailwindcss@4.1.8) + version: 1.0.0(tailwindcss@4.1.11) tailwindcss: specifier: ^4.1.5 - version: 4.1.8 + version: 4.1.11 tailwindcss-react-aria-components: specifier: ^2.0.0 - version: 2.0.0(tailwindcss@4.1.8) + version: 2.0.0(tailwindcss@4.1.11) zod: specifier: ^3.25.56 - version: 3.25.58 + version: 3.25.67 devDependencies: '@chromatic-com/storybook': specifier: ^3 - version: 3.2.6(react@19.1.0)(storybook@8.6.14(prettier@3.5.3)) + version: 3.2.7(react@19.1.0)(storybook@8.6.14(prettier@3.5.3)) '@eslint/js': specifier: ^9.25.0 - version: 9.28.0 + version: 9.30.0 '@iconify-json/ic': specifier: ^1.2.2 version: 1.2.2 @@ -68,7 +68,7 @@ importers: version: 1.2.19 '@storybook/addon-essentials': specifier: ^8.6.12 - version: 8.6.14(@types/react@19.1.7)(storybook@8.6.14(prettier@3.5.3)) + version: 8.6.14(@types/react@19.1.8)(storybook@8.6.14(prettier@3.5.3)) '@storybook/addon-onboarding': specifier: ^8.6.12 version: 8.6.14(storybook@8.6.14(prettier@3.5.3)) @@ -80,13 +80,13 @@ importers: 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)) '@storybook/experimental-addon-test': specifier: ^8.6.12 - version: 8.6.14(@vitest/browser@3.2.3)(@vitest/runner@3.2.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(storybook@8.6.14(prettier@3.5.3))(vitest@3.2.3) + version: 8.6.14(@vitest/browser@3.2.4)(@vitest/runner@3.2.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(storybook@8.6.14(prettier@3.5.3))(vitest@3.2.4) '@storybook/react': 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)(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.42.0)(storybook@8.6.14(prettier@3.5.3))(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.31)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(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.43.1)(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)) @@ -98,46 +98,46 @@ importers: version: 8.1.0(@svgr/core@8.1.0(typescript@5.8.3)) '@tanstack/router-plugin': specifier: ^1.120.2 - version: 1.121.0(@tanstack/react-router@1.121.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(vite@6.3.5(@types/node@22.15.31)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0))(webpack@5.99.9(esbuild@0.25.5)) + version: 1.123.2(@tanstack/react-router@1.123.2(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.43.1)(tsx@4.20.3)(yaml@2.8.0))(webpack@5.99.9(esbuild@0.25.5)) '@testing-library/jest-dom': specifier: ^6.6.3 version: 6.6.3 '@testing-library/react': specifier: ^16.3.0 - version: 16.3.0(@testing-library/dom@10.4.0)(@types/react-dom@19.1.6(@types/react@19.1.7))(@types/react@19.1.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + version: 16.3.0(@testing-library/dom@10.4.0)(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@types/js-cookie': specifier: ^3.0.6 version: 3.0.6 '@types/node': specifier: ^22.15.15 - version: 22.15.31 + version: 22.15.34 '@types/react': specifier: ^19.1.2 - version: 19.1.7 + version: 19.1.8 '@types/react-dom': specifier: ^19.1.2 - version: 19.1.6(@types/react@19.1.7) + version: 19.1.6(@types/react@19.1.8) '@vitejs/plugin-react': specifier: ^4.4.1 - version: 4.5.2(vite@6.3.5(@types/node@22.15.31)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(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.43.1)(tsx@4.20.3)(yaml@2.8.0)) '@vitest/browser': specifier: ^3.1.3 - version: 3.2.3(playwright@1.53.0)(vite@6.3.5(@types/node@22.15.31)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0))(vitest@3.2.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.43.1)(tsx@4.20.3)(yaml@2.8.0))(vitest@3.2.4) '@vitest/coverage-v8': specifier: ^3.1.3 - version: 3.2.3(@vitest/browser@3.2.3)(vitest@3.2.3) + version: 3.2.4(@vitest/browser@3.2.4)(vitest@3.2.4) eslint: specifier: ^9.25.0 - version: 9.28.0(jiti@2.4.2) + version: 9.30.0(jiti@2.4.2) eslint-plugin-react-hooks: specifier: ^5.2.0 - version: 5.2.0(eslint@9.28.0(jiti@2.4.2)) + version: 5.2.0(eslint@9.30.0(jiti@2.4.2)) eslint-plugin-react-refresh: specifier: ^0.4.19 - version: 0.4.20(eslint@9.28.0(jiti@2.4.2)) + version: 0.4.20(eslint@9.30.0(jiti@2.4.2)) eslint-plugin-storybook: specifier: ^0.12.0 - version: 0.12.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) + version: 0.12.0(eslint@9.30.0(jiti@2.4.2))(typescript@5.8.3) globals: specifier: ^16.0.0 version: 16.2.0 @@ -152,7 +152,7 @@ importers: version: 15.5.2 playwright: specifier: ^1.52.0 - version: 1.53.0 + version: 1.53.2 prettier: specifier: 3.5.3 version: 3.5.3 @@ -164,16 +164,16 @@ importers: version: 5.8.3 typescript-eslint: specifier: ^8.30.1 - version: 8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) + version: 8.35.1(eslint@9.30.0(jiti@2.4.2))(typescript@5.8.3) unplugin-icons: specifier: ^22.1.0 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.31)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0) + version: 6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) vitest: specifier: ^3.1.3 - version: 3.2.3(@types/node@22.15.31)(@vitest/browser@3.2.3)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(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.43.1)(tsx@4.20.3)(yaml@2.8.0) packages: @@ -197,12 +197,12 @@ packages: resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} - '@babel/compat-data@7.27.5': - resolution: {integrity: sha512-KiRAp/VoJaWkkte84TvUd9qjdbZAdiqyvMxrGl1N6vzFogKmaLgoM3L1kgtLicp2HP5fBJS8JrZKLVIZGVJAVg==} + '@babel/compat-data@7.27.7': + resolution: {integrity: sha512-xgu/ySj2mTiUFmdE9yCMfBxLp4DHd5DwmbbD05YAuICfodYT3VvRxbrh81LGQ/8UpSdtMdfKMn3KouYDX59DGQ==} engines: {node: '>=6.9.0'} - '@babel/core@7.27.4': - resolution: {integrity: sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==} + '@babel/core@7.27.7': + resolution: {integrity: sha512-BU2f9tlKQ5CAthiMIgpzAh4eDTLWo1mqi9jqE2OxMG0E/OM199VJt2q8BztTxpnSW0i1ymdwLXRJnYzvDM5r2w==} engines: {node: '>=6.9.0'} '@babel/generator@7.27.5': @@ -271,8 +271,8 @@ packages: resolution: {integrity: sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==} engines: {node: '>=6.9.0'} - '@babel/parser@7.27.5': - resolution: {integrity: sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==} + '@babel/parser@7.27.7': + resolution: {integrity: sha512-qnzXzDXdr/po3bOTbTIQZ7+TxNKxpkN5IifVLXS+r7qwynkZfPyjZfE7hCXbo7IoO9TNcSyibgONsf2HauUd3Q==} engines: {node: '>=6.0.0'} hasBin: true @@ -326,20 +326,20 @@ packages: resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.27.4': - resolution: {integrity: sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==} + '@babel/traverse@7.27.7': + resolution: {integrity: sha512-X6ZlfR/O/s5EQ/SnUSLzr+6kGnkg8HXGMzpgsMsrJVcfDtH1vIp6ctCN4eZ1LS5c0+te5Cb6Y514fASjMRJ1nw==} engines: {node: '>=6.9.0'} - '@babel/types@7.27.6': - resolution: {integrity: sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==} + '@babel/types@7.27.7': + resolution: {integrity: sha512-8OLQgDScAOHXnAz2cV+RfzzNMipuLVBz2biuAJFMV9bfkNf393je3VM8CLkjQodW5+iWsSJdSgSWT6rsZoXHPw==} engines: {node: '>=6.9.0'} '@bcoe/v8-coverage@1.0.2': resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} - '@chromatic-com/storybook@3.2.6': - resolution: {integrity: sha512-FDmn5Ry2DzQdik+eq2sp/kJMMT36Ewe7ONXUXM2Izd97c7r6R/QyGli8eyh/F0iyqVvbLveNYFyF0dBOJNwLqw==} + '@chromatic-com/storybook@3.2.7': + resolution: {integrity: sha512-fCGhk4cd3VA8RNg55MZL5CScdHqljsQcL9g6Ss7YuobHpSo9yytEWNdgMd5QxAHSPBlLGFHjnSmliM3G/BeBqw==} engines: {node: '>=16.0.0', yarn: '>=1.22.18'} peerDependencies: storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 @@ -532,32 +532,36 @@ packages: resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@eslint/config-array@0.20.0': - resolution: {integrity: sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==} + '@eslint/config-array@0.21.0': + resolution: {integrity: sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/config-helpers@0.2.2': - resolution: {integrity: sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg==} + '@eslint/config-helpers@0.3.0': + resolution: {integrity: sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/core@0.14.0': resolution: {integrity: sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/core@0.15.1': + resolution: {integrity: sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/eslintrc@3.3.1': resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@9.28.0': - resolution: {integrity: sha512-fnqSjGWd/CoIp4EXIxWVK/sHA6DOHN4+8Ix2cX5ycOY7LG0UY8nHCU5pIp2eaE1Mc7Qd8kHspYNzYXT2ojPLzg==} + '@eslint/js@9.30.0': + resolution: {integrity: sha512-Wzw3wQwPvc9sHM+NjakWTcPx11mbZyiYHuwWa/QfZ7cIRX7WK54PSk7bdyXDaoaopUcMatv1zaQvOAAO8hCdww==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/object-schema@2.1.6': resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/plugin-kit@0.3.1': - resolution: {integrity: sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==} + '@eslint/plugin-kit@0.3.3': + resolution: {integrity: sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@formatjs/ecma402-abstract@2.3.4': @@ -640,26 +644,21 @@ packages: typescript: optional: true - '@jridgewell/gen-mapping@0.3.8': - resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} - engines: {node: '>=6.0.0'} + '@jridgewell/gen-mapping@0.3.11': + resolution: {integrity: sha512-C512c1ytBTio4MrpWKlJpyFHT6+qfFL8SZ58zBzJ1OOzUEjHeF1BtjY2fH7n4x/g2OV/KiiMLAivOp1DXmiMMw==} '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} - '@jridgewell/set-array@1.2.1': - resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} - engines: {node: '>=6.0.0'} - - '@jridgewell/source-map@0.3.6': - resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==} + '@jridgewell/source-map@0.3.9': + resolution: {integrity: sha512-amBU75CKOOkcQLfyM6J+DnWwz41yTsWI7o8MQ003LwUIWb4NYX/evAblTx1oBBYJySqL/zHPxHXDw5ewpQaUFw==} - '@jridgewell/sourcemap-codec@1.5.0': - resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + '@jridgewell/sourcemap-codec@1.5.3': + resolution: {integrity: sha512-AiR5uKpFxP3PjO4R19kQGIMwxyRyPuXmKEEy301V1C0+1rVjS94EZQXf1QKZYN8Q0YM+estSPhmx5JwNftv6nw==} - '@jridgewell/trace-mapping@0.3.25': - resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + '@jridgewell/trace-mapping@0.3.28': + resolution: {integrity: sha512-KNNHHwW3EIp4EDYOvYFGyIFfx36R2dNJYH4knnZlF8T5jdbD5Wx8xmSaQ2gP9URkJ04LGEtlcCtwArKcmFcwKw==} '@mdx-js/react@3.1.0': resolution: {integrity: sha512-QjHtSaoameoalGnKDT3FoIl4+9RwyTmo9ZJGBdLOks/YOiWHoRDI3PUwEzOE7kEmGcV3AFcp9K6dYu9rEuKLAQ==} @@ -1272,11 +1271,11 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - '@rolldown/pluginutils@1.0.0-beta.11': - resolution: {integrity: sha512-L/gAA/hyCSuzTF1ftlzUSI/IKr2POHsv1Dd78GfqkR83KMNuswWD61JxGV2L7nRwBBBSDr6R1gCkdTmoN7W4ag==} + '@rolldown/pluginutils@1.0.0-beta.19': + resolution: {integrity: sha512-3FL3mnMbPu0muGOCaKAhhFEYmqv9eTfPSJRJmANrCwtgK8VuxpsZDGK+m0LYAGoyO8+0j5uRe4PeyPDK1yA/hA==} - '@rollup/pluginutils@5.1.4': - resolution: {integrity: sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==} + '@rollup/pluginutils@5.2.0': + resolution: {integrity: sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw==} engines: {node: '>=14.0.0'} peerDependencies: rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 @@ -1284,103 +1283,103 @@ packages: rollup: optional: true - '@rollup/rollup-android-arm-eabi@4.42.0': - resolution: {integrity: sha512-gldmAyS9hpj+H6LpRNlcjQWbuKUtb94lodB9uCz71Jm+7BxK1VIOo7y62tZZwxhA7j1ylv/yQz080L5WkS+LoQ==} + '@rollup/rollup-android-arm-eabi@4.44.1': + resolution: {integrity: sha512-JAcBr1+fgqx20m7Fwe1DxPUl/hPkee6jA6Pl7n1v2EFiktAHenTaXl5aIFjUIEsfn9w3HE4gK1lEgNGMzBDs1w==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.42.0': - resolution: {integrity: sha512-bpRipfTgmGFdCZDFLRvIkSNO1/3RGS74aWkJJTFJBH7h3MRV4UijkaEUeOMbi9wxtxYmtAbVcnMtHTPBhLEkaw==} + '@rollup/rollup-android-arm64@4.44.1': + resolution: {integrity: sha512-RurZetXqTu4p+G0ChbnkwBuAtwAbIwJkycw1n6GvlGlBuS4u5qlr5opix8cBAYFJgaY05TWtM+LaoFggUmbZEQ==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.42.0': - resolution: {integrity: sha512-JxHtA081izPBVCHLKnl6GEA0w3920mlJPLh89NojpU2GsBSB6ypu4erFg/Wx1qbpUbepn0jY4dVWMGZM8gplgA==} + '@rollup/rollup-darwin-arm64@4.44.1': + resolution: {integrity: sha512-fM/xPesi7g2M7chk37LOnmnSTHLG/v2ggWqKj3CCA1rMA4mm5KVBT1fNoswbo1JhPuNNZrVwpTvlCVggv8A2zg==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.42.0': - resolution: {integrity: sha512-rv5UZaWVIJTDMyQ3dCEK+m0SAn6G7H3PRc2AZmExvbDvtaDc+qXkei0knQWcI3+c9tEs7iL/4I4pTQoPbNL2SA==} + '@rollup/rollup-darwin-x64@4.44.1': + resolution: {integrity: sha512-gDnWk57urJrkrHQ2WVx9TSVTH7lSlU7E3AFqiko+bgjlh78aJ88/3nycMax52VIVjIm3ObXnDL2H00e/xzoipw==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.42.0': - resolution: {integrity: sha512-fJcN4uSGPWdpVmvLuMtALUFwCHgb2XiQjuECkHT3lWLZhSQ3MBQ9pq+WoWeJq2PrNxr9rPM1Qx+IjyGj8/c6zQ==} + '@rollup/rollup-freebsd-arm64@4.44.1': + resolution: {integrity: sha512-wnFQmJ/zPThM5zEGcnDcCJeYJgtSLjh1d//WuHzhf6zT3Md1BvvhJnWoy+HECKu2bMxaIcfWiu3bJgx6z4g2XA==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.42.0': - resolution: {integrity: sha512-CziHfyzpp8hJpCVE/ZdTizw58gr+m7Y2Xq5VOuCSrZR++th2xWAz4Nqk52MoIIrV3JHtVBhbBsJcAxs6NammOQ==} + '@rollup/rollup-freebsd-x64@4.44.1': + resolution: {integrity: sha512-uBmIxoJ4493YATvU2c0upGz87f99e3wop7TJgOA/bXMFd2SvKCI7xkxY/5k50bv7J6dw1SXT4MQBQSLn8Bb/Uw==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.42.0': - resolution: {integrity: sha512-UsQD5fyLWm2Fe5CDM7VPYAo+UC7+2Px4Y+N3AcPh/LdZu23YcuGPegQly++XEVaC8XUTFVPscl5y5Cl1twEI4A==} + '@rollup/rollup-linux-arm-gnueabihf@4.44.1': + resolution: {integrity: sha512-n0edDmSHlXFhrlmTK7XBuwKlG5MbS7yleS1cQ9nn4kIeW+dJH+ExqNgQ0RrFRew8Y+0V/x6C5IjsHrJmiHtkxQ==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.42.0': - resolution: {integrity: sha512-/i8NIrlgc/+4n1lnoWl1zgH7Uo0XK5xK3EDqVTf38KvyYgCU/Rm04+o1VvvzJZnVS5/cWSd07owkzcVasgfIkQ==} + '@rollup/rollup-linux-arm-musleabihf@4.44.1': + resolution: {integrity: sha512-8WVUPy3FtAsKSpyk21kV52HCxB+me6YkbkFHATzC2Yd3yuqHwy2lbFL4alJOLXKljoRw08Zk8/xEj89cLQ/4Nw==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.42.0': - resolution: {integrity: sha512-eoujJFOvoIBjZEi9hJnXAbWg+Vo1Ov8n/0IKZZcPZ7JhBzxh2A+2NFyeMZIRkY9iwBvSjloKgcvnjTbGKHE44Q==} + '@rollup/rollup-linux-arm64-gnu@4.44.1': + resolution: {integrity: sha512-yuktAOaeOgorWDeFJggjuCkMGeITfqvPgkIXhDqsfKX8J3jGyxdDZgBV/2kj/2DyPaLiX6bPdjJDTu9RB8lUPQ==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.42.0': - resolution: {integrity: sha512-/3NrcOWFSR7RQUQIuZQChLND36aTU9IYE4j+TB40VU78S+RA0IiqHR30oSh6P1S9f9/wVOenHQnacs/Byb824g==} + '@rollup/rollup-linux-arm64-musl@4.44.1': + resolution: {integrity: sha512-W+GBM4ifET1Plw8pdVaecwUgxmiH23CfAUj32u8knq0JPFyK4weRy6H7ooxYFD19YxBulL0Ktsflg5XS7+7u9g==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-loongarch64-gnu@4.42.0': - resolution: {integrity: sha512-O8AplvIeavK5ABmZlKBq9/STdZlnQo7Sle0LLhVA7QT+CiGpNVe197/t8Aph9bhJqbDVGCHpY2i7QyfEDDStDg==} + '@rollup/rollup-linux-loongarch64-gnu@4.44.1': + resolution: {integrity: sha512-1zqnUEMWp9WrGVuVak6jWTl4fEtrVKfZY7CvcBmUUpxAJ7WcSowPSAWIKa/0o5mBL/Ij50SIf9tuirGx63Ovew==} cpu: [loong64] os: [linux] - '@rollup/rollup-linux-powerpc64le-gnu@4.42.0': - resolution: {integrity: sha512-6Qb66tbKVN7VyQrekhEzbHRxXXFFD8QKiFAwX5v9Xt6FiJ3BnCVBuyBxa2fkFGqxOCSGGYNejxd8ht+q5SnmtA==} + '@rollup/rollup-linux-powerpc64le-gnu@4.44.1': + resolution: {integrity: sha512-Rl3JKaRu0LHIx7ExBAAnf0JcOQetQffaw34T8vLlg9b1IhzcBgaIdnvEbbsZq9uZp3uAH+JkHd20Nwn0h9zPjA==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.42.0': - resolution: {integrity: sha512-KQETDSEBamQFvg/d8jajtRwLNBlGc3aKpaGiP/LvEbnmVUKlFta1vqJqTrvPtsYsfbE/DLg5CC9zyXRX3fnBiA==} + '@rollup/rollup-linux-riscv64-gnu@4.44.1': + resolution: {integrity: sha512-j5akelU3snyL6K3N/iX7otLBIl347fGwmd95U5gS/7z6T4ftK288jKq3A5lcFKcx7wwzb5rgNvAg3ZbV4BqUSw==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-riscv64-musl@4.42.0': - resolution: {integrity: sha512-qMvnyjcU37sCo/tuC+JqeDKSuukGAd+pVlRl/oyDbkvPJ3awk6G6ua7tyum02O3lI+fio+eM5wsVd66X0jQtxw==} + '@rollup/rollup-linux-riscv64-musl@4.44.1': + resolution: {integrity: sha512-ppn5llVGgrZw7yxbIm8TTvtj1EoPgYUAbfw0uDjIOzzoqlZlZrLJ/KuiE7uf5EpTpCTrNt1EdtzF0naMm0wGYg==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.42.0': - resolution: {integrity: sha512-I2Y1ZUgTgU2RLddUHXTIgyrdOwljjkmcZ/VilvaEumtS3Fkuhbw4p4hgHc39Ypwvo2o7sBFNl2MquNvGCa55Iw==} + '@rollup/rollup-linux-s390x-gnu@4.44.1': + resolution: {integrity: sha512-Hu6hEdix0oxtUma99jSP7xbvjkUM/ycke/AQQ4EC5g7jNRLLIwjcNwaUy95ZKBJJwg1ZowsclNnjYqzN4zwkAw==} cpu: [s390x] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.42.0': - resolution: {integrity: sha512-Gfm6cV6mj3hCUY8TqWa63DB8Mx3NADoFwiJrMpoZ1uESbK8FQV3LXkhfry+8bOniq9pqY1OdsjFWNsSbfjPugw==} + '@rollup/rollup-linux-x64-gnu@4.44.1': + resolution: {integrity: sha512-EtnsrmZGomz9WxK1bR5079zee3+7a+AdFlghyd6VbAjgRJDbTANJ9dcPIPAi76uG05micpEL+gPGmAKYTschQw==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.42.0': - resolution: {integrity: sha512-g86PF8YZ9GRqkdi0VoGlcDUb4rYtQKyTD1IVtxxN4Hpe7YqLBShA7oHMKU6oKTCi3uxwW4VkIGnOaH/El8de3w==} + '@rollup/rollup-linux-x64-musl@4.44.1': + resolution: {integrity: sha512-iAS4p+J1az6Usn0f8xhgL4PaU878KEtutP4hqw52I4IO6AGoyOkHCxcc4bqufv1tQLdDWFx8lR9YlwxKuv3/3g==} cpu: [x64] os: [linux] - '@rollup/rollup-win32-arm64-msvc@4.42.0': - resolution: {integrity: sha512-+axkdyDGSp6hjyzQ5m1pgcvQScfHnMCcsXkx8pTgy/6qBmWVhtRVlgxjWwDp67wEXXUr0x+vD6tp5W4x6V7u1A==} + '@rollup/rollup-win32-arm64-msvc@4.44.1': + resolution: {integrity: sha512-NtSJVKcXwcqozOl+FwI41OH3OApDyLk3kqTJgx8+gp6On9ZEt5mYhIsKNPGuaZr3p9T6NWPKGU/03Vw4CNU9qg==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.42.0': - resolution: {integrity: sha512-F+5J9pelstXKwRSDq92J0TEBXn2nfUrQGg+HK1+Tk7VOL09e0gBqUHugZv7SW4MGrYj41oNCUe3IKCDGVlis2g==} + '@rollup/rollup-win32-ia32-msvc@4.44.1': + resolution: {integrity: sha512-JYA3qvCOLXSsnTR3oiyGws1Dm0YTuxAAeaYGVlGpUsHqloPcFjPg+X0Fj2qODGLNwQOAcCiQmHub/V007kiH5A==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.42.0': - resolution: {integrity: sha512-LpHiJRwkaVz/LqjHjK8LCi8osq7elmpwujwbXKNW88bM8eeGxavJIKKjkjpMHAh/2xfnrt1ZSnhTv41WYUHYmA==} + '@rollup/rollup-win32-x64-msvc@4.44.1': + resolution: {integrity: sha512-J8o22LuF0kTe7m+8PvW9wk3/bRq5+mRo5Dqo6+vXb7otCm3TPhYOJqOaQtGU9YMWQSL3krMnoOxMr0+9E6F3Ug==} cpu: [x64] os: [win32] @@ -1644,65 +1643,65 @@ packages: '@swc/helpers@0.5.17': resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==} - '@tailwindcss/node@4.1.8': - resolution: {integrity: sha512-OWwBsbC9BFAJelmnNcrKuf+bka2ZxCE2A4Ft53Tkg4uoiE67r/PMEYwCsourC26E+kmxfwE0hVzMdxqeW+xu7Q==} + '@tailwindcss/node@4.1.11': + resolution: {integrity: sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q==} - '@tailwindcss/oxide-android-arm64@4.1.8': - resolution: {integrity: sha512-Fbz7qni62uKYceWYvUjRqhGfZKwhZDQhlrJKGtnZfuNtHFqa8wmr+Wn74CTWERiW2hn3mN5gTpOoxWKk0jRxjg==} + '@tailwindcss/oxide-android-arm64@4.1.11': + resolution: {integrity: sha512-3IfFuATVRUMZZprEIx9OGDjG3Ou3jG4xQzNTvjDoKmU9JdmoCohQJ83MYd0GPnQIu89YoJqvMM0G3uqLRFtetg==} engines: {node: '>= 10'} cpu: [arm64] os: [android] - '@tailwindcss/oxide-darwin-arm64@4.1.8': - resolution: {integrity: sha512-RdRvedGsT0vwVVDztvyXhKpsU2ark/BjgG0huo4+2BluxdXo8NDgzl77qh0T1nUxmM11eXwR8jA39ibvSTbi7A==} + '@tailwindcss/oxide-darwin-arm64@4.1.11': + resolution: {integrity: sha512-ESgStEOEsyg8J5YcMb1xl8WFOXfeBmrhAwGsFxxB2CxY9evy63+AtpbDLAyRkJnxLy2WsD1qF13E97uQyP1lfQ==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@tailwindcss/oxide-darwin-x64@4.1.8': - resolution: {integrity: sha512-t6PgxjEMLp5Ovf7uMb2OFmb3kqzVTPPakWpBIFzppk4JE4ix0yEtbtSjPbU8+PZETpaYMtXvss2Sdkx8Vs4XRw==} + '@tailwindcss/oxide-darwin-x64@4.1.11': + resolution: {integrity: sha512-EgnK8kRchgmgzG6jE10UQNaH9Mwi2n+yw1jWmof9Vyg2lpKNX2ioe7CJdf9M5f8V9uaQxInenZkOxnTVL3fhAw==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@tailwindcss/oxide-freebsd-x64@4.1.8': - resolution: {integrity: sha512-g8C8eGEyhHTqwPStSwZNSrOlyx0bhK/V/+zX0Y+n7DoRUzyS8eMbVshVOLJTDDC+Qn9IJnilYbIKzpB9n4aBsg==} + '@tailwindcss/oxide-freebsd-x64@4.1.11': + resolution: {integrity: sha512-xdqKtbpHs7pQhIKmqVpxStnY1skuNh4CtbcyOHeX1YBE0hArj2romsFGb6yUmzkq/6M24nkxDqU8GYrKrz+UcA==} engines: {node: '>= 10'} cpu: [x64] os: [freebsd] - '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.8': - resolution: {integrity: sha512-Jmzr3FA4S2tHhaC6yCjac3rGf7hG9R6Gf2z9i9JFcuyy0u79HfQsh/thifbYTF2ic82KJovKKkIB6Z9TdNhCXQ==} + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.11': + resolution: {integrity: sha512-ryHQK2eyDYYMwB5wZL46uoxz2zzDZsFBwfjssgB7pzytAeCCa6glsiJGjhTEddq/4OsIjsLNMAiMlHNYnkEEeg==} engines: {node: '>= 10'} cpu: [arm] os: [linux] - '@tailwindcss/oxide-linux-arm64-gnu@4.1.8': - resolution: {integrity: sha512-qq7jXtO1+UEtCmCeBBIRDrPFIVI4ilEQ97qgBGdwXAARrUqSn/L9fUrkb1XP/mvVtoVeR2bt/0L77xx53bPZ/Q==} + '@tailwindcss/oxide-linux-arm64-gnu@4.1.11': + resolution: {integrity: sha512-mYwqheq4BXF83j/w75ewkPJmPZIqqP1nhoghS9D57CLjsh3Nfq0m4ftTotRYtGnZd3eCztgbSPJ9QhfC91gDZQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@tailwindcss/oxide-linux-arm64-musl@4.1.8': - resolution: {integrity: sha512-O6b8QesPbJCRshsNApsOIpzKt3ztG35gfX9tEf4arD7mwNinsoCKxkj8TgEE0YRjmjtO3r9FlJnT/ENd9EVefQ==} + '@tailwindcss/oxide-linux-arm64-musl@4.1.11': + resolution: {integrity: sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@tailwindcss/oxide-linux-x64-gnu@4.1.8': - resolution: {integrity: sha512-32iEXX/pXwikshNOGnERAFwFSfiltmijMIAbUhnNyjFr3tmWmMJWQKU2vNcFX0DACSXJ3ZWcSkzNbaKTdngH6g==} + '@tailwindcss/oxide-linux-x64-gnu@4.1.11': + resolution: {integrity: sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@tailwindcss/oxide-linux-x64-musl@4.1.8': - resolution: {integrity: sha512-s+VSSD+TfZeMEsCaFaHTaY5YNj3Dri8rST09gMvYQKwPphacRG7wbuQ5ZJMIJXN/puxPcg/nU+ucvWguPpvBDg==} + '@tailwindcss/oxide-linux-x64-musl@4.1.11': + resolution: {integrity: sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@tailwindcss/oxide-wasm32-wasi@4.1.8': - resolution: {integrity: sha512-CXBPVFkpDjM67sS1psWohZ6g/2/cd+cq56vPxK4JeawelxwK4YECgl9Y9TjkE2qfF+9/s1tHHJqrC4SS6cVvSg==} + '@tailwindcss/oxide-wasm32-wasi@4.1.11': + resolution: {integrity: sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g==} engines: {node: '>=14.0.0'} cpu: [wasm32] bundledDependencies: @@ -1713,41 +1712,41 @@ packages: - '@emnapi/wasi-threads' - tslib - '@tailwindcss/oxide-win32-arm64-msvc@4.1.8': - resolution: {integrity: sha512-7GmYk1n28teDHUjPlIx4Z6Z4hHEgvP5ZW2QS9ygnDAdI/myh3HTHjDqtSqgu1BpRoI4OiLx+fThAyA1JePoENA==} + '@tailwindcss/oxide-win32-arm64-msvc@4.1.11': + resolution: {integrity: sha512-UgKYx5PwEKrac3GPNPf6HVMNhUIGuUh4wlDFR2jYYdkX6pL/rn73zTq/4pzUm8fOjAn5L8zDeHp9iXmUGOXZ+w==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@tailwindcss/oxide-win32-x64-msvc@4.1.8': - resolution: {integrity: sha512-fou+U20j+Jl0EHwK92spoWISON2OBnCazIc038Xj2TdweYV33ZRkS9nwqiUi2d/Wba5xg5UoHfvynnb/UB49cQ==} + '@tailwindcss/oxide-win32-x64-msvc@4.1.11': + resolution: {integrity: sha512-YfHoggn1j0LK7wR82TOucWc5LDCguHnoS879idHekmmiR7g9HUtMw9MI0NHatS28u/Xlkfi9w5RJWgz2Dl+5Qg==} engines: {node: '>= 10'} cpu: [x64] os: [win32] - '@tailwindcss/oxide@4.1.8': - resolution: {integrity: sha512-d7qvv9PsM5N3VNKhwVUhpK6r4h9wtLkJ6lz9ZY9aeZgrUWk1Z8VPyqyDT9MZlem7GTGseRQHkeB1j3tC7W1P+A==} + '@tailwindcss/oxide@4.1.11': + resolution: {integrity: sha512-Q69XzrtAhuyfHo+5/HMgr1lAiPP/G40OMFAnws7xcFEYqcypZmdW8eGXaOUIeOl1dzPJBPENXgbjsOyhg2nkrg==} engines: {node: '>= 10'} - '@tailwindcss/vite@4.1.8': - resolution: {integrity: sha512-CQ+I8yxNV5/6uGaJjiuymgw0kEQiNKRinYbZXPdx1fk5WgiyReG0VaUx/Xq6aVNSUNJFzxm6o8FNKS5aMaim5A==} + '@tailwindcss/vite@4.1.11': + resolution: {integrity: sha512-RHYhrR3hku0MJFRV+fN2gNbDNEh3dwKvY8XJvTxCSXeMOsCRSr+uKvDWQcbizrHgjML6ZmTE5OwMrl5wKcujCw==} peerDependencies: - vite: ^5.2.0 || ^6 + vite: ^5.2.0 || ^6 || ^7 - '@tanstack/history@1.120.17': - resolution: {integrity: sha512-k07LFI4Qo074IIaWzT/XjD0KlkGx2w1V3fnNtclKx0oAl8z4O9kCh6za+FPEIRe98xLgNFEiddDbJeAYGSlPtw==} + '@tanstack/history@1.121.34': + resolution: {integrity: sha512-YL8dGi5ZU+xvtav2boRlw4zrRghkY6hvdcmHhA0RGSJ/CBgzv+cbADW9eYJLx74XMZvIQ1pp6VMbrpXnnM5gHA==} engines: {node: '>=12'} - '@tanstack/query-core@5.80.6': - resolution: {integrity: sha512-nl7YxT/TAU+VTf+e2zTkObGTyY8YZBMnbgeA1ee66lIVqzKlYursAII6z5t0e6rXgwUMJSV4dshBTNacNpZHbQ==} + '@tanstack/query-core@5.81.5': + resolution: {integrity: sha512-ZJOgCy/z2qpZXWaj/oxvodDx07XcQa9BF92c0oINjHkoqUPsmm3uG08HpTaviviZ/N9eP1f9CM7mKSEkIo7O1Q==} - '@tanstack/react-query@5.80.6': - resolution: {integrity: sha512-izX+5CnkpON3NQGcEm3/d7LfFQNo9ZpFtX2QsINgCYK9LT2VCIdi8D3bMaMSNhrAJCznRoAkFic76uvLroALBw==} + '@tanstack/react-query@5.81.5': + resolution: {integrity: sha512-lOf2KqRRiYWpQT86eeeftAGnjuTR35myTP8MXyvHa81VlomoAWNEd8x5vkcAfQefu0qtYCvyqLropFZqgI2EQw==} peerDependencies: react: ^18 || ^19 - '@tanstack/react-router@1.121.0': - resolution: {integrity: sha512-l+hwNPzAPuCb/V4K6E1ZwKplnk8/nYUTQrXdtfXro5xOT1zNedJOm+juZsNKyoHokvhH+uT+I1s+mrkaykctcA==} + '@tanstack/react-router@1.123.2': + resolution: {integrity: sha512-IaFcVdK1kf/KyH43gR9osk9Zp9ms7cE0xz8iqeRQIH94rNDY67aJZkYgT5WKmoaPOkVokNii38TSehe2sNYkdw==} engines: {node: '>=12'} peerDependencies: react: '>=18.0.0 || >=19.0.0' @@ -1759,20 +1758,20 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - '@tanstack/router-core@1.121.0': - resolution: {integrity: sha512-EMiLgRMHbi1JHOgJOrxUSc3Ws+Jge3bGa7r03tdIgvH07dJcsY03L5ZLtuqYKvuS6YJvYeciEXA31IUIEN9dVA==} + '@tanstack/router-core@1.123.2': + resolution: {integrity: sha512-k1GTymZ2CBOX9SDiVHlqtgrNiid8fSKGjAofLToMAoLgqESg1knuIsYSeivmPkVbeSSdKkwF2uqsrJqNRIp/TQ==} engines: {node: '>=12'} - '@tanstack/router-generator@1.121.0': - resolution: {integrity: sha512-3JDHlL5mrdVI6RwRf92YId0fgYr6Vdh0yo/pYMUV6USkB0fRtayz176AODvTaR5IeB2BYkaiPKTK38ySyGueRw==} + '@tanstack/router-generator@1.123.2': + resolution: {integrity: sha512-JJ1spXiYlUndXhZUwSpkwWwytvnhm164KP8mUbXHfY6STcH5doBZTcwkjUm5MYz8TXta3rNCuutNTshEMQX1BQ==} engines: {node: '>=12'} - '@tanstack/router-plugin@1.121.0': - resolution: {integrity: sha512-A05CE2JuebgU+D7H4VHhOkWDmW1NxFbKVq82hHewiBWdq2nwODSZQV4AO+Ub7bIcILHYX/ivKuspK11s/d7k0A==} + '@tanstack/router-plugin@1.123.2': + resolution: {integrity: sha512-5+oM8Xmv0EAOs/ZU0wbKKzbpwiqixco1mAg3DCy/R5z4IIFq9Df9N33zC7c2RRDjJ/S2OzXWWA3Dn26DuC/HUg==} engines: {node: '>=12'} peerDependencies: '@rsbuild/core': '>=1.0.2' - '@tanstack/react-router': ^1.121.0 + '@tanstack/react-router': ^1.123.2 vite: '>=5.0.0 || >=6.0.0' vite-plugin-solid: ^2.11.2 webpack: '>=5.92.0' @@ -1788,15 +1787,15 @@ packages: webpack: optional: true - '@tanstack/router-utils@1.121.0': - resolution: {integrity: sha512-+gOHZdEVjOTTdk8Z7J/NVG0KdvzxFeUYjINYZEqQDRKoxEg8f+Npram0MXGy8N15OyZrsm+KHR1vMFZ2yEvZkw==} + '@tanstack/router-utils@1.121.21': + resolution: {integrity: sha512-u7ubq1xPBtNiU7Fm+EOWlVWdgFLzuKOa1thhqdscVn8R4dNMUd1VoOjZ6AKmLw201VaUhFtlX+u0pjzI6szX7A==} engines: {node: '>=12'} '@tanstack/store@0.7.1': resolution: {integrity: sha512-PjUQKXEXhLYj2X5/6c1Xn/0/qKY0IVFxTJweopRfF26xfjVyb14yALydJrHupDh3/d+1WKmfEgZPBVCmDkzzwg==} - '@tanstack/virtual-file-routes@1.120.17': - resolution: {integrity: sha512-Ssi+yKcjG9ru02ieCpUBF7QQBEKGB7WQS1R9va3GHu+Oq9WjzmJ4rifzdugjTeKD3yfT7d1I+pOxRhoWog6CHw==} + '@tanstack/virtual-file-routes@1.121.21': + resolution: {integrity: sha512-3nuYsTyaq6ZN7jRZ9z6Gj3GXZqBOqOT0yzd/WZ33ZFfv4yVNIvsa5Lw+M1j3sgyEAxKMqGu/FaNi7FCjr3yOdw==} engines: {node: '>=12'} '@testing-library/dom@10.4.0': @@ -1868,9 +1867,6 @@ packages: '@types/eslint@9.6.1': resolution: {integrity: sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==} - '@types/estree@1.0.7': - resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} - '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -1883,16 +1879,16 @@ packages: '@types/mdx@2.0.13': resolution: {integrity: sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==} - '@types/node@22.15.31': - resolution: {integrity: sha512-jnVe5ULKl6tijxUhvQeNbQG/84fHfg+yMak02cT8QVhBx/F05rAVxCGBYYTh2EKz22D6JF5ktXuNwdx7b9iEGw==} + '@types/node@22.15.34': + resolution: {integrity: sha512-8Y6E5WUupYy1Dd0II32BsWAx5MWdcnRd8L84Oys3veg1YrYtNtzgO4CFhiBg6MDSjk7Ay36HYOnU7/tuOzIzcw==} '@types/react-dom@19.1.6': resolution: {integrity: sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==} peerDependencies: '@types/react': ^19.0.0 - '@types/react@19.1.7': - resolution: {integrity: sha512-BnsPLV43ddr05N71gaGzyZ5hzkCmGwhMvYc8zmvI8Ci1bRkkDSzDDVfAXfN2tk748OwI7ediiPX6PfT9p0QGVg==} + '@types/react@19.1.8': + resolution: {integrity: sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==} '@types/resolve@1.20.6': resolution: {integrity: sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==} @@ -1900,77 +1896,77 @@ packages: '@types/uuid@9.0.8': resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==} - '@typescript-eslint/eslint-plugin@8.34.0': - resolution: {integrity: sha512-QXwAlHlbcAwNlEEMKQS2RCgJsgXrTJdjXT08xEgbPFa2yYQgVjBymxP5DrfrE7X7iodSzd9qBUHUycdyVJTW1w==} + '@typescript-eslint/eslint-plugin@8.35.1': + resolution: {integrity: sha512-9XNTlo7P7RJxbVeICaIIIEipqxLKguyh+3UbXuT2XQuFp6d8VOeDEGuz5IiX0dgZo8CiI6aOFLg4e8cF71SFVg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.34.0 + '@typescript-eslint/parser': ^8.35.1 eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/parser@8.34.0': - resolution: {integrity: sha512-vxXJV1hVFx3IXz/oy2sICsJukaBrtDEQSBiV48/YIV5KWjX1dO+bcIr/kCPrW6weKXvsaGKFNlwH0v2eYdRRbA==} + '@typescript-eslint/parser@8.35.1': + resolution: {integrity: sha512-3MyiDfrfLeK06bi/g9DqJxP5pV74LNv4rFTyvGDmT3x2p1yp1lOd+qYZfiRPIOf/oON+WRZR5wxxuF85qOar+w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/project-service@8.34.0': - resolution: {integrity: sha512-iEgDALRf970/B2YExmtPMPF54NenZUf4xpL3wsCRx/lgjz6ul/l13R81ozP/ZNuXfnLCS+oPmG7JIxfdNYKELw==} + '@typescript-eslint/project-service@8.35.1': + resolution: {integrity: sha512-VYxn/5LOpVxADAuP3NrnxxHYfzVtQzLKeldIhDhzC8UHaiQvYlXvKuVho1qLduFbJjjy5U5bkGwa3rUGUb1Q6Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/scope-manager@8.34.0': - resolution: {integrity: sha512-9Ac0X8WiLykl0aj1oYQNcLZjHgBojT6cW68yAgZ19letYu+Hxd0rE0veI1XznSSst1X5lwnxhPbVdwjDRIomRw==} + '@typescript-eslint/scope-manager@8.35.1': + resolution: {integrity: sha512-s/Bpd4i7ht2934nG+UoSPlYXd08KYz3bmjLEb7Ye1UVob0d1ENiT3lY8bsCmik4RqfSbPw9xJJHbugpPpP5JUg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.34.0': - resolution: {integrity: sha512-+W9VYHKFIzA5cBeooqQxqNriAP0QeQ7xTiDuIOr71hzgffm3EL2hxwWBIIj4GuofIbKxGNarpKqIq6Q6YrShOA==} + '@typescript-eslint/tsconfig-utils@8.35.1': + resolution: {integrity: sha512-K5/U9VmT9dTHoNowWZpz+/TObS3xqC5h0xAIjXPw+MNcKV9qg6eSatEnmeAwkjHijhACH0/N7bkhKvbt1+DXWQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/type-utils@8.34.0': - resolution: {integrity: sha512-n7zSmOcUVhcRYC75W2pnPpbO1iwhJY3NLoHEtbJwJSNlVAZuwqu05zY3f3s2SDWWDSo9FdN5szqc73DCtDObAg==} + '@typescript-eslint/type-utils@8.35.1': + resolution: {integrity: sha512-HOrUBlfVRz5W2LIKpXzZoy6VTZzMu2n8q9C2V/cFngIC5U1nStJgv0tMV4sZPzdf4wQm9/ToWUFPMN9Vq9VJQQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/types@8.34.0': - resolution: {integrity: sha512-9V24k/paICYPniajHfJ4cuAWETnt7Ssy+R0Rbcqo5sSFr3QEZ/8TSoUi9XeXVBGXCaLtwTOKSLGcInCAvyZeMA==} + '@typescript-eslint/types@8.35.1': + resolution: {integrity: sha512-q/O04vVnKHfrrhNAscndAn1tuQhIkwqnaW+eu5waD5IPts2eX1dgJxgqcPx5BX109/qAz7IG6VrEPTOYKCNfRQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.34.0': - resolution: {integrity: sha512-rOi4KZxI7E0+BMqG7emPSK1bB4RICCpF7QD3KCLXn9ZvWoESsOMlHyZPAHyG04ujVplPaHbmEvs34m+wjgtVtg==} + '@typescript-eslint/typescript-estree@8.35.1': + resolution: {integrity: sha512-Vvpuvj4tBxIka7cPs6Y1uvM7gJgdF5Uu9F+mBJBPY4MhvjrjWGK4H0lVgLJd/8PWZ23FTqsaJaLEkBCFUk8Y9g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/utils@8.34.0': - resolution: {integrity: sha512-8L4tWatGchV9A1cKbjaavS6mwYwp39jql8xUmIIKJdm+qiaeHy5KMKlBrf30akXAWBzn2SqKsNOtSENWUwg7XQ==} + '@typescript-eslint/utils@8.35.1': + resolution: {integrity: sha512-lhnwatFmOFcazAsUm3ZnZFpXSxiwoa1Lj50HphnDe1Et01NF4+hrdXONSUHIcbVu2eFb1bAf+5yjXkGVkXBKAQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/visitor-keys@8.34.0': - resolution: {integrity: sha512-qHV7pW7E85A0x6qyrFn+O+q1k1p3tQCsqIZ1KZ5ESLXY57aTvUd3/a4rdPTeXisvhXn2VQG0VSKUqs8KHF2zcA==} + '@typescript-eslint/visitor-keys@8.35.1': + resolution: {integrity: sha512-VRwixir4zBWCSTP/ljEo091lbpypz57PoeAQ9imjG+vbeof9LplljsL1mos4ccG6H9IjfrVGM359RozUnuFhpw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@vitejs/plugin-react@4.5.2': - resolution: {integrity: sha512-QNVT3/Lxx99nMQWJWF7K4N6apUEuT0KlZA3mx/mVaoGj3smm/8rc8ezz15J1pcbcjDK0V15rpHetVfya08r76Q==} + '@vitejs/plugin-react@4.6.0': + resolution: {integrity: sha512-5Kgff+m8e2PB+9j51eGHEpn5kUzRKH2Ry0qGoe8ItJg7pqnkPrYPkDQZGgGmTa0EGarHrkjLvOdU3b1fzI8otQ==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0 - '@vitest/browser@3.2.3': - resolution: {integrity: sha512-5HpUb0ixGF8JWSAjb/P1x/VPuTYUkL4pL0+YO6DJiuvQgqJN3PREaUEcXwfXjU4nBc37EahfpRbAwdE9pHs9lQ==} + '@vitest/browser@3.2.4': + resolution: {integrity: sha512-tJxiPrWmzH8a+w9nLKlQMzAKX/7VjFs50MWgcAj7p9XQ7AQ9/35fByFYptgPELyLw+0aixTnC4pUWV+APcZ/kw==} peerDependencies: playwright: '*' safaridriver: '*' - vitest: 3.2.3 + vitest: 3.2.4 webdriverio: ^7.0.0 || ^8.0.0 || ^9.0.0 peerDependenciesMeta: playwright: @@ -1980,11 +1976,11 @@ packages: webdriverio: optional: true - '@vitest/coverage-v8@3.2.3': - resolution: {integrity: sha512-D1QKzngg8PcDoCE8FHSZhREDuEy+zcKmMiMafYse41RZpBE5EDJyKOTdqK3RQfsV2S2nyKor5KCs8PyPRFqKPg==} + '@vitest/coverage-v8@3.2.4': + resolution: {integrity: sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==} peerDependencies: - '@vitest/browser': 3.2.3 - vitest: 3.2.3 + '@vitest/browser': 3.2.4 + vitest: 3.2.4 peerDependenciesMeta: '@vitest/browser': optional: true @@ -1992,11 +1988,11 @@ packages: '@vitest/expect@2.0.5': resolution: {integrity: sha512-yHZtwuP7JZivj65Gxoi8upUN2OzHTi3zVfjwdpu2WrvCZPLwsJ2Ey5ILIPccoW23dd/zQBlJ4/dhi7DWNyXCpA==} - '@vitest/expect@3.2.3': - resolution: {integrity: sha512-W2RH2TPWVHA1o7UmaFKISPvdicFJH+mjykctJFoAkUw+SPTJTGjUNdKscFBrqM7IPnCVu6zihtKYa7TkZS1dkQ==} + '@vitest/expect@3.2.4': + resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} - '@vitest/mocker@3.2.3': - resolution: {integrity: sha512-cP6fIun+Zx8he4rbWvi+Oya6goKQDZK+Yq4hhlggwQBbrlOQ4qtZ+G4nxB6ZnzI9lyIb+JnvyiJnPC2AGbKSPA==} + '@vitest/mocker@3.2.4': + resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==} peerDependencies: msw: ^2.4.9 vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 @@ -2012,20 +2008,20 @@ packages: '@vitest/pretty-format@2.1.9': resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==} - '@vitest/pretty-format@3.2.3': - resolution: {integrity: sha512-yFglXGkr9hW/yEXngO+IKMhP0jxyFw2/qys/CK4fFUZnSltD+MU7dVYGrH8rvPcK/O6feXQA+EU33gjaBBbAng==} + '@vitest/pretty-format@3.2.4': + resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} - '@vitest/runner@3.2.3': - resolution: {integrity: sha512-83HWYisT3IpMaU9LN+VN+/nLHVBCSIUKJzGxC5RWUOsK1h3USg7ojL+UXQR3b4o4UBIWCYdD2fxuzM7PQQ1u8w==} + '@vitest/runner@3.2.4': + resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==} - '@vitest/snapshot@3.2.3': - resolution: {integrity: sha512-9gIVWx2+tysDqUmmM1L0hwadyumqssOL1r8KJipwLx5JVYyxvVRfxvMq7DaWbZZsCqZnu/dZedaZQh4iYTtneA==} + '@vitest/snapshot@3.2.4': + resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==} '@vitest/spy@2.0.5': resolution: {integrity: sha512-c/jdthAhvJdpfVuaexSrnawxZz6pywlTPe84LUB2m/4t3rl2fTo9NFGBG4oWgaD+FTgDDV8hJ/nibT7IfH3JfA==} - '@vitest/spy@3.2.3': - resolution: {integrity: sha512-JHu9Wl+7bf6FEejTCREy+DmgWe+rQKbK+y32C/k5f4TBIAlijhJbRBIRIOCEpVevgRsCQR2iHRUH2/qKVM/plw==} + '@vitest/spy@3.2.4': + resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} '@vitest/utils@2.0.5': resolution: {integrity: sha512-d8HKbqIcya+GR67mkZbrzhS5kKhtp8dQLcmRZLGTscGVg7yImT82cIrhtn2L8+VujWcy6KZweApgNmPsTAO/UQ==} @@ -2033,8 +2029,8 @@ packages: '@vitest/utils@2.1.9': resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} - '@vitest/utils@3.2.3': - resolution: {integrity: sha512-4zFBCU5Pf+4Z6v+rwnZ1HU1yzOKKvDkMXZrymE2PBlbjKJRlrOxbvpfPSvJTGRIwGoahaOGvp+kbCoxifhzJ1Q==} + '@vitest/utils@3.2.4': + resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} '@webassemblyjs/ast@1.14.1': resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} @@ -2180,8 +2176,8 @@ packages: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} - axios@1.9.0: - resolution: {integrity: sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==} + axios@1.10.0: + resolution: {integrity: sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==} babel-dead-code-elimination@1.0.10: resolution: {integrity: sha512-DV5bdJZTzZ0zn0DC24v3jD7Mnidh6xhKa4GfKCbq3sfW8kaWhDdZjP3i81geA8T33tdYqWKw4D3fVv0CwEgKVA==} @@ -2197,11 +2193,11 @@ packages: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} - brace-expansion@1.1.11: - resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} - brace-expansion@2.0.1: - resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} @@ -2210,8 +2206,8 @@ packages: browser-assert@1.2.1: resolution: {integrity: sha512-nfulgvOR6S4gt9UKCeGJOuSGBPGiFT6oQ/2UBnvTY/5aQ1PnksW72fhZkM30DzoRRv2WpwZf1vHHEr3mtuXIWQ==} - browserslist@4.25.0: - resolution: {integrity: sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==} + browserslist@4.25.1: + resolution: {integrity: sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -2242,8 +2238,8 @@ packages: resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} engines: {node: '>=10'} - caniuse-lite@1.0.30001721: - resolution: {integrity: sha512-cOuvmUVtKrtEaoKiO0rSc29jcjwMwX5tOHDy4MgVFEWiUXj4uBMJkwI8MDySkgXidpMiHUcviogAvFi4pA2hDQ==} + caniuse-lite@1.0.30001726: + resolution: {integrity: sha512-VQAUIUzBiZ/UnlM28fSp2CRF3ivUn1BWEvxMcVTNwpw91Py1pGbPIyIKtd+tzct9C3ouceCVdGAXxZOpZAsgdw==} chai@5.2.0: resolution: {integrity: sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==} @@ -2337,6 +2333,9 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie-es@1.2.2: + resolution: {integrity: sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==} + cosmiconfig@8.3.6: resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==} engines: {node: '>=14'} @@ -2353,8 +2352,8 @@ packages: css.escape@1.5.1: resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} - cssstyle@4.4.0: - resolution: {integrity: sha512-W0Y2HOXlPkb2yaKrCVRjinYKciu/qSLEmK0K9mcfDei3zwlnHFEHAs/Du3cIRwPqY+J4JsiBzUjoHyc8RsJ03A==} + cssstyle@4.6.0: + resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==} engines: {node: '>=18'} csstype@3.1.3: @@ -2427,8 +2426,8 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - electron-to-chromium@1.5.166: - resolution: {integrity: sha512-QPWqHL0BglzPYyJJ1zSSmwFFL6MFXhbACOCcsCdUMCkzPdS9/OIBVxg516X/Ado2qwAq8k0nJJ7phQPCqiaFAw==} + electron-to-chromium@1.5.178: + resolution: {integrity: sha512-wObbz/ar3Bc6e4X5vf0iO8xTN8YAjN/tgiAOJLr7yjYFtP9wAjq8Mb5h0yn6kResir+VYx2DXBj9NNobs0ETSA==} emoji-regex@10.4.0: resolution: {integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==} @@ -2439,8 +2438,8 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - enhanced-resolve@5.18.1: - resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==} + enhanced-resolve@5.18.2: + resolution: {integrity: sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==} engines: {node: '>=10.13.0'} entities@4.5.0: @@ -2528,8 +2527,8 @@ packages: resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint@9.28.0: - resolution: {integrity: sha512-ocgh41VhRlf9+fVpe7QKzwLj9c92fDiqOj8Y3Sd4/ZmVA4Btx4PlUYPq4pp9JDyupkf1upbEXecxL2mwNV7jPQ==} + eslint@9.30.0: + resolution: {integrity: sha512-iN/SiPxmQu6EVkf+m1qpBxzUhE12YqFLOSySuOyVLJLEF9nzTf+h/1AJYc1JWzCnktggeNrjvQGLngDzXirU6g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true peerDependencies: @@ -2883,6 +2882,10 @@ packages: resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} engines: {node: '>=8'} + isbot@5.1.28: + resolution: {integrity: sha512-qrOp4g3xj8YNse4biorv6O5ZShwsJM0trsoda4y7j/Su7ZtTTfVXFzbKkpgcSoDrHS8FcTuUwcU04YimZlZOxw==} + engines: {node: '>=18'} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -3088,8 +3091,8 @@ packages: resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} engines: {node: '>=18'} - loupe@3.1.3: - resolution: {integrity: sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==} + loupe@3.1.4: + resolution: {integrity: sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg==} lower-case@2.0.2: resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} @@ -3287,8 +3290,8 @@ packages: pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} - pathval@2.0.0: - resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==} + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} engines: {node: '>= 14.16'} picocolors@1.1.1: @@ -3310,16 +3313,16 @@ packages: pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} - pkg-types@2.1.1: - resolution: {integrity: sha512-eY0QFb6eSwc9+0d/5D2lFFUq+A3n3QNGSy/X2Nvp+6MfzGw2u6EbA7S80actgjY1lkvvI0pqB+a4hioMh443Ew==} + pkg-types@2.2.0: + resolution: {integrity: sha512-2SM/GZGAEkPp3KWORxQZns4M+WSeXbC2HEvmOIJe3Cmiv6ieAJvdVhDldtHqM5J1Y7MrR1XhkBT/rMlhh9FdqQ==} - playwright-core@1.53.0: - resolution: {integrity: sha512-mGLg8m0pm4+mmtB7M89Xw/GSqoNC+twivl8ITteqvAndachozYe2ZA7srU6uleV1vEdAHYqjq+SV8SNxRRFYBw==} + playwright-core@1.53.2: + resolution: {integrity: sha512-ox/OytMy+2w1jcYEYlOo1Hhp8hZkLCximMTUTMBXjGUA1KoFfiSZ+DU+3a739jsPY0yoKH2TFy9S2fsJas8yAw==} engines: {node: '>=18'} hasBin: true - playwright@1.53.0: - resolution: {integrity: sha512-ghGNnIEYZC4E+YtclRn4/p6oYbdPiASELBIYkBXfaTVKreQUYbMUYQDwS12a8F0/HtIjr/CkGjtwABeFPGcS4Q==} + playwright@1.53.2: + resolution: {integrity: sha512-6K/qQxVFuVQhRQhFsVZ9fGeatxirtrpPgxzBYWyZLEXJzqYwuL4fuNmfOfD5et1tJE4GScKyPNeLhZeRwuTU3A==} engines: {node: '>=18'} hasBin: true @@ -3331,8 +3334,8 @@ packages: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} - postcss@8.5.4: - resolution: {integrity: sha512-QSa9EBe+uwlGTFmHsPKokv3B/oEMQZxfqW0QqNCyhpa6mB1afzulwn8hihglqAb2pOw+BJgNlmXQ8la2VeHB7w==} + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} prelude-ls@1.2.1: @@ -3459,8 +3462,8 @@ packages: rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} - rollup@4.42.0: - resolution: {integrity: sha512-LW+Vse3BJPyGJGAJt1j8pWDKPd73QM8cRXYK1IxOBgL2AGLu7Xd2YOW0M2sLUBCkF5MshXXtMApyEAEzMVMsnw==} + rollup@4.44.1: + resolution: {integrity: sha512-x8H8aPvD+xbl0Do8oez5f5o8eMS3trfCghc4HhLAnCkj7Vl0d1JWGs0UF/D886zLW2rOj2QymV/JcSSsw+XDNg==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -3652,8 +3655,8 @@ packages: peerDependencies: tailwindcss: ^4.0.0 - tailwindcss@4.1.8: - resolution: {integrity: sha512-kjeW8gjdxasbmFKpVGrGd5T4i40mV5J2Rasw48QARfYeQ8YS9x02ON9SFWax3Qf616rt4Cp3nVNIj6Hd1mP3og==} + tailwindcss@4.1.11: + resolution: {integrity: sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==} tapable@2.2.2: resolution: {integrity: sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==} @@ -3679,8 +3682,8 @@ packages: uglify-js: optional: true - terser@5.42.0: - resolution: {integrity: sha512-UYCvU9YQW2f/Vwl+P0GfhxJxbUGLwd+5QrrGgLajzWAtC/23AX0vcise32kkP7Eu0Wu9VlzzHAXkLObgjQfFlQ==} + terser@5.43.1: + resolution: {integrity: sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==} engines: {node: '>=10'} hasBin: true @@ -3707,8 +3710,8 @@ packages: resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} engines: {node: '>=12.0.0'} - tinypool@1.1.0: - resolution: {integrity: sha512-7CotroY9a8DKsKprEy/a14aCCm8jYVmR7aFy4fpkZM8sdpNJbKkixuNjgM50yCmip2ezc8z4N7k3oe2+rfRJCQ==} + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} engines: {node: ^18.0.0 || >=20.0.0} tinyrainbow@1.2.0: @@ -3767,8 +3770,8 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - tsx@4.19.4: - resolution: {integrity: sha512-gK5GVzDkJK1SI1zwHf32Mqxf2tSJkNx+eYcNly5+nHvWqXUJYUkWBQtKauoESz3ymezAI++ZwT855x5p5eop+Q==} + tsx@4.20.3: + resolution: {integrity: sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==} engines: {node: '>=18.0.0'} hasBin: true @@ -3783,8 +3786,8 @@ packages: resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} engines: {node: '>=12.20'} - typescript-eslint@8.34.0: - resolution: {integrity: sha512-MRpfN7uYjTrTGigFCt8sRyNqJFhjN0WwZecldaqhWm+wy0gaRt8Edb/3cuUy0zdq2opJWT6iXINKAtewnDOltQ==} + typescript-eslint@8.35.1: + resolution: {integrity: sha512-xslJjFzhOmHYQzSB/QTeASAHbjmxOGEP6Coh93TXmUBFQoJ1VU35UHIDmG06Jd6taf3wqqC1ntBnCMeymy5Ovw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -3857,8 +3860,8 @@ packages: resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} hasBin: true - vite-node@3.2.3: - resolution: {integrity: sha512-gc8aAifGuDIpZHrPjuHyP4dpQmYXqWw7D1GmDnWeNWP654UEXzVfQ5IHPSK5HaHkwB/+p1atpYpSdw/2kOv8iQ==} + vite-node@3.2.4: + resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true @@ -3902,16 +3905,16 @@ packages: yaml: optional: true - vitest@3.2.3: - resolution: {integrity: sha512-E6U2ZFXe3N/t4f5BwUaVCKRLHqUpk1CBWeMh78UT4VaTPH/2dyvH6ALl29JTovEPu9dVKr/K/J4PkXgrMbw4Ww==} + vitest@3.2.4: + resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@types/debug': ^4.1.12 '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 - '@vitest/browser': 3.2.3 - '@vitest/ui': 3.2.3 + '@vitest/browser': 3.2.4 + '@vitest/ui': 3.2.4 happy-dom: '*' jsdom: '*' peerDependenciesMeta: @@ -3942,8 +3945,8 @@ packages: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} engines: {node: '>=12'} - webpack-sources@3.3.2: - resolution: {integrity: sha512-ykKKus8lqlgXX/1WjudpIEjqsafjOTcOJqxnAbMLAu/KCsDCJ6GBtvscewvTkrn24HsnvFwrSCbenFrhtcCsAA==} + webpack-sources@3.3.3: + resolution: {integrity: sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==} engines: {node: '>=10.13.0'} webpack-virtual-modules@0.6.2: @@ -4001,8 +4004,8 @@ packages: resolution: {integrity: sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==} engines: {node: '>=18'} - ws@8.18.2: - resolution: {integrity: sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==} + ws@8.18.3: + resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -4036,8 +4039,8 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} - zod@3.25.58: - resolution: {integrity: sha512-DVLmMQzSZwNYzQoMaM3MQWnxr2eq+AtM9Hx3w1/Yl0pH8sLTSjN4jGP7w6f7uand6Hw44tsnSu1hz1AOA6qI2Q==} + zod@3.25.67: + resolution: {integrity: sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw==} snapshots: @@ -4045,8 +4048,8 @@ snapshots: '@ampproject/remapping@2.3.0': dependencies: - '@jridgewell/gen-mapping': 0.3.8 - '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/gen-mapping': 0.3.11 + '@jridgewell/trace-mapping': 0.3.28 '@antfu/install-pkg@1.1.0': dependencies: @@ -4069,20 +4072,20 @@ snapshots: js-tokens: 4.0.0 picocolors: 1.1.1 - '@babel/compat-data@7.27.5': {} + '@babel/compat-data@7.27.7': {} - '@babel/core@7.27.4': + '@babel/core@7.27.7': dependencies: '@ampproject/remapping': 2.3.0 '@babel/code-frame': 7.27.1 '@babel/generator': 7.27.5 '@babel/helper-compilation-targets': 7.27.2 - '@babel/helper-module-transforms': 7.27.3(@babel/core@7.27.4) + '@babel/helper-module-transforms': 7.27.3(@babel/core@7.27.7) '@babel/helpers': 7.27.6 - '@babel/parser': 7.27.5 + '@babel/parser': 7.27.7 '@babel/template': 7.27.2 - '@babel/traverse': 7.27.4 - '@babel/types': 7.27.6 + '@babel/traverse': 7.27.7 + '@babel/types': 7.27.7 convert-source-map: 2.0.0 debug: 4.4.1 gensync: 1.0.0-beta.2 @@ -4093,79 +4096,79 @@ snapshots: '@babel/generator@7.27.5': dependencies: - '@babel/parser': 7.27.5 - '@babel/types': 7.27.6 - '@jridgewell/gen-mapping': 0.3.8 - '@jridgewell/trace-mapping': 0.3.25 + '@babel/parser': 7.27.7 + '@babel/types': 7.27.7 + '@jridgewell/gen-mapping': 0.3.11 + '@jridgewell/trace-mapping': 0.3.28 jsesc: 3.1.0 '@babel/helper-annotate-as-pure@7.27.3': dependencies: - '@babel/types': 7.27.6 + '@babel/types': 7.27.7 '@babel/helper-compilation-targets@7.27.2': dependencies: - '@babel/compat-data': 7.27.5 + '@babel/compat-data': 7.27.7 '@babel/helper-validator-option': 7.27.1 - browserslist: 4.25.0 + browserslist: 4.25.1 lru-cache: 5.1.1 semver: 6.3.1 - '@babel/helper-create-class-features-plugin@7.27.1(@babel/core@7.27.4)': + '@babel/helper-create-class-features-plugin@7.27.1(@babel/core@7.27.7)': dependencies: - '@babel/core': 7.27.4 + '@babel/core': 7.27.7 '@babel/helper-annotate-as-pure': 7.27.3 '@babel/helper-member-expression-to-functions': 7.27.1 '@babel/helper-optimise-call-expression': 7.27.1 - '@babel/helper-replace-supers': 7.27.1(@babel/core@7.27.4) + '@babel/helper-replace-supers': 7.27.1(@babel/core@7.27.7) '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - '@babel/traverse': 7.27.4 + '@babel/traverse': 7.27.7 semver: 6.3.1 transitivePeerDependencies: - supports-color '@babel/helper-member-expression-to-functions@7.27.1': dependencies: - '@babel/traverse': 7.27.4 - '@babel/types': 7.27.6 + '@babel/traverse': 7.27.7 + '@babel/types': 7.27.7 transitivePeerDependencies: - supports-color '@babel/helper-module-imports@7.27.1': dependencies: - '@babel/traverse': 7.27.4 - '@babel/types': 7.27.6 + '@babel/traverse': 7.27.7 + '@babel/types': 7.27.7 transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.27.3(@babel/core@7.27.4)': + '@babel/helper-module-transforms@7.27.3(@babel/core@7.27.7)': dependencies: - '@babel/core': 7.27.4 + '@babel/core': 7.27.7 '@babel/helper-module-imports': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 - '@babel/traverse': 7.27.4 + '@babel/traverse': 7.27.7 transitivePeerDependencies: - supports-color '@babel/helper-optimise-call-expression@7.27.1': dependencies: - '@babel/types': 7.27.6 + '@babel/types': 7.27.7 '@babel/helper-plugin-utils@7.27.1': {} - '@babel/helper-replace-supers@7.27.1(@babel/core@7.27.4)': + '@babel/helper-replace-supers@7.27.1(@babel/core@7.27.7)': dependencies: - '@babel/core': 7.27.4 + '@babel/core': 7.27.7 '@babel/helper-member-expression-to-functions': 7.27.1 '@babel/helper-optimise-call-expression': 7.27.1 - '@babel/traverse': 7.27.4 + '@babel/traverse': 7.27.7 transitivePeerDependencies: - supports-color '@babel/helper-skip-transparent-expression-wrappers@7.27.1': dependencies: - '@babel/traverse': 7.27.4 - '@babel/types': 7.27.6 + '@babel/traverse': 7.27.7 + '@babel/types': 7.27.7 transitivePeerDependencies: - supports-color @@ -4178,59 +4181,59 @@ snapshots: '@babel/helpers@7.27.6': dependencies: '@babel/template': 7.27.2 - '@babel/types': 7.27.6 + '@babel/types': 7.27.7 - '@babel/parser@7.27.5': + '@babel/parser@7.27.7': dependencies: - '@babel/types': 7.27.6 + '@babel/types': 7.27.7 - '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.27.4)': + '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.27.7)': dependencies: - '@babel/core': 7.27.4 + '@babel/core': 7.27.7 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.27.4)': + '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.27.7)': dependencies: - '@babel/core': 7.27.4 + '@babel/core': 7.27.7 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-modules-commonjs@7.27.1(@babel/core@7.27.4)': + '@babel/plugin-transform-modules-commonjs@7.27.1(@babel/core@7.27.7)': dependencies: - '@babel/core': 7.27.4 - '@babel/helper-module-transforms': 7.27.3(@babel/core@7.27.4) + '@babel/core': 7.27.7 + '@babel/helper-module-transforms': 7.27.3(@babel/core@7.27.7) '@babel/helper-plugin-utils': 7.27.1 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.27.4)': + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.27.7)': dependencies: - '@babel/core': 7.27.4 + '@babel/core': 7.27.7 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.27.4)': + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.27.7)': dependencies: - '@babel/core': 7.27.4 + '@babel/core': 7.27.7 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-typescript@7.27.1(@babel/core@7.27.4)': + '@babel/plugin-transform-typescript@7.27.1(@babel/core@7.27.7)': dependencies: - '@babel/core': 7.27.4 + '@babel/core': 7.27.7 '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.27.4) + '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.27.7) '@babel/helper-plugin-utils': 7.27.1 '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.27.7) transitivePeerDependencies: - supports-color - '@babel/preset-typescript@7.27.1(@babel/core@7.27.4)': + '@babel/preset-typescript@7.27.1(@babel/core@7.27.7)': dependencies: - '@babel/core': 7.27.4 + '@babel/core': 7.27.7 '@babel/helper-plugin-utils': 7.27.1 '@babel/helper-validator-option': 7.27.1 - '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.27.4) - '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.27.4) - '@babel/plugin-transform-typescript': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.27.7) + '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.27.7) + '@babel/plugin-transform-typescript': 7.27.1(@babel/core@7.27.7) transitivePeerDependencies: - supports-color @@ -4239,29 +4242,29 @@ snapshots: '@babel/template@7.27.2': dependencies: '@babel/code-frame': 7.27.1 - '@babel/parser': 7.27.5 - '@babel/types': 7.27.6 + '@babel/parser': 7.27.7 + '@babel/types': 7.27.7 - '@babel/traverse@7.27.4': + '@babel/traverse@7.27.7': dependencies: '@babel/code-frame': 7.27.1 '@babel/generator': 7.27.5 - '@babel/parser': 7.27.5 + '@babel/parser': 7.27.7 '@babel/template': 7.27.2 - '@babel/types': 7.27.6 + '@babel/types': 7.27.7 debug: 4.4.1 globals: 11.12.0 transitivePeerDependencies: - supports-color - '@babel/types@7.27.6': + '@babel/types@7.27.7': dependencies: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 '@bcoe/v8-coverage@1.0.2': {} - '@chromatic-com/storybook@3.2.6(react@19.1.0)(storybook@8.6.14(prettier@3.5.3))': + '@chromatic-com/storybook@3.2.7(react@19.1.0)(storybook@8.6.14(prettier@3.5.3))': dependencies: chromatic: 11.29.0 filesize: 10.1.6 @@ -4369,14 +4372,14 @@ snapshots: '@esbuild/win32-x64@0.25.5': optional: true - '@eslint-community/eslint-utils@4.7.0(eslint@9.28.0(jiti@2.4.2))': + '@eslint-community/eslint-utils@4.7.0(eslint@9.30.0(jiti@2.4.2))': dependencies: - eslint: 9.28.0(jiti@2.4.2) + eslint: 9.30.0(jiti@2.4.2) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.1': {} - '@eslint/config-array@0.20.0': + '@eslint/config-array@0.21.0': dependencies: '@eslint/object-schema': 2.1.6 debug: 4.4.1 @@ -4384,12 +4387,16 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/config-helpers@0.2.2': {} + '@eslint/config-helpers@0.3.0': {} '@eslint/core@0.14.0': dependencies: '@types/json-schema': 7.0.15 + '@eslint/core@0.15.1': + dependencies: + '@types/json-schema': 7.0.15 + '@eslint/eslintrc@3.3.1': dependencies: ajv: 6.12.6 @@ -4404,13 +4411,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/js@9.28.0': {} + '@eslint/js@9.30.0': {} '@eslint/object-schema@2.1.6': {} - '@eslint/plugin-kit@0.3.1': + '@eslint/plugin-kit@0.3.3': dependencies: - '@eslint/core': 0.14.0 + '@eslint/core': 0.15.1 levn: 0.4.1 '@formatjs/ecma402-abstract@2.3.4': @@ -4507,41 +4514,38 @@ 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.31)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(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.43.1)(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.31)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) optionalDependencies: typescript: 5.8.3 - '@jridgewell/gen-mapping@0.3.8': + '@jridgewell/gen-mapping@0.3.11': dependencies: - '@jridgewell/set-array': 1.2.1 - '@jridgewell/sourcemap-codec': 1.5.0 - '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/sourcemap-codec': 1.5.3 + '@jridgewell/trace-mapping': 0.3.28 '@jridgewell/resolve-uri@3.1.2': {} - '@jridgewell/set-array@1.2.1': {} - - '@jridgewell/source-map@0.3.6': + '@jridgewell/source-map@0.3.9': dependencies: - '@jridgewell/gen-mapping': 0.3.8 - '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/gen-mapping': 0.3.11 + '@jridgewell/trace-mapping': 0.3.28 - '@jridgewell/sourcemap-codec@1.5.0': {} + '@jridgewell/sourcemap-codec@1.5.3': {} - '@jridgewell/trace-mapping@0.3.25': + '@jridgewell/trace-mapping@0.3.28': dependencies: '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/sourcemap-codec': 1.5.3 - '@mdx-js/react@3.1.0(@types/react@19.1.7)(react@19.1.0)': + '@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.7 + '@types/react': 19.1.8 react: 19.1.0 '@nodelib/fs.scandir@2.1.5': @@ -5604,74 +5608,74 @@ snapshots: '@react-types/shared': 3.30.0(react@19.1.0) react: 19.1.0 - '@rolldown/pluginutils@1.0.0-beta.11': {} + '@rolldown/pluginutils@1.0.0-beta.19': {} - '@rollup/pluginutils@5.1.4(rollup@4.42.0)': + '@rollup/pluginutils@5.2.0(rollup@4.44.1)': dependencies: '@types/estree': 1.0.8 estree-walker: 2.0.2 picomatch: 4.0.2 optionalDependencies: - rollup: 4.42.0 + rollup: 4.44.1 - '@rollup/rollup-android-arm-eabi@4.42.0': + '@rollup/rollup-android-arm-eabi@4.44.1': optional: true - '@rollup/rollup-android-arm64@4.42.0': + '@rollup/rollup-android-arm64@4.44.1': optional: true - '@rollup/rollup-darwin-arm64@4.42.0': + '@rollup/rollup-darwin-arm64@4.44.1': optional: true - '@rollup/rollup-darwin-x64@4.42.0': + '@rollup/rollup-darwin-x64@4.44.1': optional: true - '@rollup/rollup-freebsd-arm64@4.42.0': + '@rollup/rollup-freebsd-arm64@4.44.1': optional: true - '@rollup/rollup-freebsd-x64@4.42.0': + '@rollup/rollup-freebsd-x64@4.44.1': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.42.0': + '@rollup/rollup-linux-arm-gnueabihf@4.44.1': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.42.0': + '@rollup/rollup-linux-arm-musleabihf@4.44.1': optional: true - '@rollup/rollup-linux-arm64-gnu@4.42.0': + '@rollup/rollup-linux-arm64-gnu@4.44.1': optional: true - '@rollup/rollup-linux-arm64-musl@4.42.0': + '@rollup/rollup-linux-arm64-musl@4.44.1': optional: true - '@rollup/rollup-linux-loongarch64-gnu@4.42.0': + '@rollup/rollup-linux-loongarch64-gnu@4.44.1': optional: true - '@rollup/rollup-linux-powerpc64le-gnu@4.42.0': + '@rollup/rollup-linux-powerpc64le-gnu@4.44.1': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.42.0': + '@rollup/rollup-linux-riscv64-gnu@4.44.1': optional: true - '@rollup/rollup-linux-riscv64-musl@4.42.0': + '@rollup/rollup-linux-riscv64-musl@4.44.1': optional: true - '@rollup/rollup-linux-s390x-gnu@4.42.0': + '@rollup/rollup-linux-s390x-gnu@4.44.1': optional: true - '@rollup/rollup-linux-x64-gnu@4.42.0': + '@rollup/rollup-linux-x64-gnu@4.44.1': optional: true - '@rollup/rollup-linux-x64-musl@4.42.0': + '@rollup/rollup-linux-x64-musl@4.44.1': optional: true - '@rollup/rollup-win32-arm64-msvc@4.42.0': + '@rollup/rollup-win32-arm64-msvc@4.44.1': optional: true - '@rollup/rollup-win32-ia32-msvc@4.42.0': + '@rollup/rollup-win32-ia32-msvc@4.44.1': optional: true - '@rollup/rollup-win32-x64-msvc@4.42.0': + '@rollup/rollup-win32-x64-msvc@4.44.1': optional: true '@storybook/addon-actions@8.6.14(storybook@8.6.14(prettier@3.5.3))': @@ -5697,9 +5701,9 @@ snapshots: storybook: 8.6.14(prettier@3.5.3) ts-dedent: 2.2.0 - '@storybook/addon-docs@8.6.14(@types/react@19.1.7)(storybook@8.6.14(prettier@3.5.3))': + '@storybook/addon-docs@8.6.14(@types/react@19.1.8)(storybook@8.6.14(prettier@3.5.3))': dependencies: - '@mdx-js/react': 3.1.0(@types/react@19.1.7)(react@19.1.0) + '@mdx-js/react': 3.1.0(@types/react@19.1.8)(react@19.1.0) '@storybook/blocks': 8.6.14(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(storybook@8.6.14(prettier@3.5.3)) '@storybook/csf-plugin': 8.6.14(storybook@8.6.14(prettier@3.5.3)) '@storybook/react-dom-shim': 8.6.14(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(storybook@8.6.14(prettier@3.5.3)) @@ -5710,12 +5714,12 @@ snapshots: transitivePeerDependencies: - '@types/react' - '@storybook/addon-essentials@8.6.14(@types/react@19.1.7)(storybook@8.6.14(prettier@3.5.3))': + '@storybook/addon-essentials@8.6.14(@types/react@19.1.8)(storybook@8.6.14(prettier@3.5.3))': dependencies: '@storybook/addon-actions': 8.6.14(storybook@8.6.14(prettier@3.5.3)) '@storybook/addon-backgrounds': 8.6.14(storybook@8.6.14(prettier@3.5.3)) '@storybook/addon-controls': 8.6.14(storybook@8.6.14(prettier@3.5.3)) - '@storybook/addon-docs': 8.6.14(@types/react@19.1.7)(storybook@8.6.14(prettier@3.5.3)) + '@storybook/addon-docs': 8.6.14(@types/react@19.1.8)(storybook@8.6.14(prettier@3.5.3)) '@storybook/addon-highlight': 8.6.14(storybook@8.6.14(prettier@3.5.3)) '@storybook/addon-measure': 8.6.14(storybook@8.6.14(prettier@3.5.3)) '@storybook/addon-outline': 8.6.14(storybook@8.6.14(prettier@3.5.3)) @@ -5772,13 +5776,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.31)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(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.43.1)(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.31)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) '@storybook/components@8.6.14(storybook@8.6.14(prettier@3.5.3))': dependencies: @@ -5796,7 +5800,7 @@ snapshots: recast: 0.23.11 semver: 7.7.2 util: 0.12.5 - ws: 8.18.2 + ws: 8.18.3 optionalDependencies: prettier: 3.5.3 transitivePeerDependencies: @@ -5814,7 +5818,7 @@ snapshots: dependencies: type-fest: 2.19.0 - '@storybook/experimental-addon-test@8.6.14(@vitest/browser@3.2.3)(@vitest/runner@3.2.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(storybook@8.6.14(prettier@3.5.3))(vitest@3.2.3)': + '@storybook/experimental-addon-test@8.6.14(@vitest/browser@3.2.4)(@vitest/runner@3.2.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(storybook@8.6.14(prettier@3.5.3))(vitest@3.2.4)': dependencies: '@storybook/global': 5.0.0 '@storybook/icons': 1.4.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -5825,9 +5829,9 @@ snapshots: storybook: 8.6.14(prettier@3.5.3) ts-dedent: 2.2.0 optionalDependencies: - '@vitest/browser': 3.2.3(playwright@1.53.0)(vite@6.3.5(@types/node@22.15.31)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0))(vitest@3.2.3) - '@vitest/runner': 3.2.3 - vitest: 3.2.3(@types/node@22.15.31)(@vitest/browser@3.2.3)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0) + '@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.43.1)(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.43.1)(tsx@4.20.3)(yaml@2.8.0) transitivePeerDependencies: - react - react-dom @@ -5863,11 +5867,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.42.0)(storybook@8.6.14(prettier@3.5.3))(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.31)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(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.43.1)(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.31)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0)) - '@rollup/pluginutils': 5.1.4(rollup@4.42.0) - '@storybook/builder-vite': 8.6.14(storybook@8.6.14(prettier@3.5.3))(vite@6.3.5(@types/node@22.15.31)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(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.43.1)(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.43.1)(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 @@ -5877,7 +5881,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.31)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) optionalDependencies: '@storybook/test': 8.6.14(storybook@8.6.14(prettier@3.5.3)) transitivePeerDependencies: @@ -5915,54 +5919,54 @@ snapshots: dependencies: storybook: 8.6.14(prettier@3.5.3) - '@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.27.4)': + '@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.27.7)': dependencies: - '@babel/core': 7.27.4 + '@babel/core': 7.27.7 - '@svgr/babel-plugin-remove-jsx-attribute@8.0.0(@babel/core@7.27.4)': + '@svgr/babel-plugin-remove-jsx-attribute@8.0.0(@babel/core@7.27.7)': dependencies: - '@babel/core': 7.27.4 + '@babel/core': 7.27.7 - '@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0(@babel/core@7.27.4)': + '@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0(@babel/core@7.27.7)': dependencies: - '@babel/core': 7.27.4 + '@babel/core': 7.27.7 - '@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0(@babel/core@7.27.4)': + '@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0(@babel/core@7.27.7)': dependencies: - '@babel/core': 7.27.4 + '@babel/core': 7.27.7 - '@svgr/babel-plugin-svg-dynamic-title@8.0.0(@babel/core@7.27.4)': + '@svgr/babel-plugin-svg-dynamic-title@8.0.0(@babel/core@7.27.7)': dependencies: - '@babel/core': 7.27.4 + '@babel/core': 7.27.7 - '@svgr/babel-plugin-svg-em-dimensions@8.0.0(@babel/core@7.27.4)': + '@svgr/babel-plugin-svg-em-dimensions@8.0.0(@babel/core@7.27.7)': dependencies: - '@babel/core': 7.27.4 + '@babel/core': 7.27.7 - '@svgr/babel-plugin-transform-react-native-svg@8.1.0(@babel/core@7.27.4)': + '@svgr/babel-plugin-transform-react-native-svg@8.1.0(@babel/core@7.27.7)': dependencies: - '@babel/core': 7.27.4 + '@babel/core': 7.27.7 - '@svgr/babel-plugin-transform-svg-component@8.0.0(@babel/core@7.27.4)': + '@svgr/babel-plugin-transform-svg-component@8.0.0(@babel/core@7.27.7)': dependencies: - '@babel/core': 7.27.4 + '@babel/core': 7.27.7 - '@svgr/babel-preset@8.1.0(@babel/core@7.27.4)': + '@svgr/babel-preset@8.1.0(@babel/core@7.27.7)': dependencies: - '@babel/core': 7.27.4 - '@svgr/babel-plugin-add-jsx-attribute': 8.0.0(@babel/core@7.27.4) - '@svgr/babel-plugin-remove-jsx-attribute': 8.0.0(@babel/core@7.27.4) - '@svgr/babel-plugin-remove-jsx-empty-expression': 8.0.0(@babel/core@7.27.4) - '@svgr/babel-plugin-replace-jsx-attribute-value': 8.0.0(@babel/core@7.27.4) - '@svgr/babel-plugin-svg-dynamic-title': 8.0.0(@babel/core@7.27.4) - '@svgr/babel-plugin-svg-em-dimensions': 8.0.0(@babel/core@7.27.4) - '@svgr/babel-plugin-transform-react-native-svg': 8.1.0(@babel/core@7.27.4) - '@svgr/babel-plugin-transform-svg-component': 8.0.0(@babel/core@7.27.4) + '@babel/core': 7.27.7 + '@svgr/babel-plugin-add-jsx-attribute': 8.0.0(@babel/core@7.27.7) + '@svgr/babel-plugin-remove-jsx-attribute': 8.0.0(@babel/core@7.27.7) + '@svgr/babel-plugin-remove-jsx-empty-expression': 8.0.0(@babel/core@7.27.7) + '@svgr/babel-plugin-replace-jsx-attribute-value': 8.0.0(@babel/core@7.27.7) + '@svgr/babel-plugin-svg-dynamic-title': 8.0.0(@babel/core@7.27.7) + '@svgr/babel-plugin-svg-em-dimensions': 8.0.0(@babel/core@7.27.7) + '@svgr/babel-plugin-transform-react-native-svg': 8.1.0(@babel/core@7.27.7) + '@svgr/babel-plugin-transform-svg-component': 8.0.0(@babel/core@7.27.7) '@svgr/core@8.1.0(typescript@5.8.3)': dependencies: - '@babel/core': 7.27.4 - '@svgr/babel-preset': 8.1.0(@babel/core@7.27.4) + '@babel/core': 7.27.7 + '@svgr/babel-preset': 8.1.0(@babel/core@7.27.7) camelcase: 6.3.0 cosmiconfig: 8.3.6(typescript@5.8.3) snake-case: 3.0.4 @@ -5972,13 +5976,13 @@ snapshots: '@svgr/hast-util-to-babel-ast@8.0.0': dependencies: - '@babel/types': 7.27.6 + '@babel/types': 7.27.7 entities: 4.5.0 '@svgr/plugin-jsx@8.1.0(@svgr/core@8.1.0(typescript@5.8.3))': dependencies: - '@babel/core': 7.27.4 - '@svgr/babel-preset': 8.1.0(@babel/core@7.27.4) + '@babel/core': 7.27.7 + '@svgr/babel-preset': 8.1.0(@babel/core@7.27.7) '@svgr/core': 8.1.0(typescript@5.8.3) '@svgr/hast-util-to-babel-ast': 8.0.0 svg-parser: 2.0.4 @@ -5989,91 +5993,92 @@ snapshots: dependencies: tslib: 2.8.1 - '@tailwindcss/node@4.1.8': + '@tailwindcss/node@4.1.11': dependencies: '@ampproject/remapping': 2.3.0 - enhanced-resolve: 5.18.1 + enhanced-resolve: 5.18.2 jiti: 2.4.2 lightningcss: 1.30.1 magic-string: 0.30.17 source-map-js: 1.2.1 - tailwindcss: 4.1.8 + tailwindcss: 4.1.11 - '@tailwindcss/oxide-android-arm64@4.1.8': + '@tailwindcss/oxide-android-arm64@4.1.11': optional: true - '@tailwindcss/oxide-darwin-arm64@4.1.8': + '@tailwindcss/oxide-darwin-arm64@4.1.11': optional: true - '@tailwindcss/oxide-darwin-x64@4.1.8': + '@tailwindcss/oxide-darwin-x64@4.1.11': optional: true - '@tailwindcss/oxide-freebsd-x64@4.1.8': + '@tailwindcss/oxide-freebsd-x64@4.1.11': optional: true - '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.8': + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.11': optional: true - '@tailwindcss/oxide-linux-arm64-gnu@4.1.8': + '@tailwindcss/oxide-linux-arm64-gnu@4.1.11': optional: true - '@tailwindcss/oxide-linux-arm64-musl@4.1.8': + '@tailwindcss/oxide-linux-arm64-musl@4.1.11': optional: true - '@tailwindcss/oxide-linux-x64-gnu@4.1.8': + '@tailwindcss/oxide-linux-x64-gnu@4.1.11': optional: true - '@tailwindcss/oxide-linux-x64-musl@4.1.8': + '@tailwindcss/oxide-linux-x64-musl@4.1.11': optional: true - '@tailwindcss/oxide-wasm32-wasi@4.1.8': + '@tailwindcss/oxide-wasm32-wasi@4.1.11': optional: true - '@tailwindcss/oxide-win32-arm64-msvc@4.1.8': + '@tailwindcss/oxide-win32-arm64-msvc@4.1.11': optional: true - '@tailwindcss/oxide-win32-x64-msvc@4.1.8': + '@tailwindcss/oxide-win32-x64-msvc@4.1.11': optional: true - '@tailwindcss/oxide@4.1.8': + '@tailwindcss/oxide@4.1.11': dependencies: detect-libc: 2.0.4 tar: 7.4.3 optionalDependencies: - '@tailwindcss/oxide-android-arm64': 4.1.8 - '@tailwindcss/oxide-darwin-arm64': 4.1.8 - '@tailwindcss/oxide-darwin-x64': 4.1.8 - '@tailwindcss/oxide-freebsd-x64': 4.1.8 - '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.8 - '@tailwindcss/oxide-linux-arm64-gnu': 4.1.8 - '@tailwindcss/oxide-linux-arm64-musl': 4.1.8 - '@tailwindcss/oxide-linux-x64-gnu': 4.1.8 - '@tailwindcss/oxide-linux-x64-musl': 4.1.8 - '@tailwindcss/oxide-wasm32-wasi': 4.1.8 - '@tailwindcss/oxide-win32-arm64-msvc': 4.1.8 - '@tailwindcss/oxide-win32-x64-msvc': 4.1.8 + '@tailwindcss/oxide-android-arm64': 4.1.11 + '@tailwindcss/oxide-darwin-arm64': 4.1.11 + '@tailwindcss/oxide-darwin-x64': 4.1.11 + '@tailwindcss/oxide-freebsd-x64': 4.1.11 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.11 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.11 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.11 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.11 + '@tailwindcss/oxide-linux-x64-musl': 4.1.11 + '@tailwindcss/oxide-wasm32-wasi': 4.1.11 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.11 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.11 - '@tailwindcss/vite@4.1.8(vite@6.3.5(@types/node@22.15.31)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(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.43.1)(tsx@4.20.3)(yaml@2.8.0))': dependencies: - '@tailwindcss/node': 4.1.8 - '@tailwindcss/oxide': 4.1.8 - tailwindcss: 4.1.8 - vite: 6.3.5(@types/node@22.15.31)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0) + '@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.43.1)(tsx@4.20.3)(yaml@2.8.0) - '@tanstack/history@1.120.17': {} + '@tanstack/history@1.121.34': {} - '@tanstack/query-core@5.80.6': {} + '@tanstack/query-core@5.81.5': {} - '@tanstack/react-query@5.80.6(react@19.1.0)': + '@tanstack/react-query@5.81.5(react@19.1.0)': dependencies: - '@tanstack/query-core': 5.80.6 + '@tanstack/query-core': 5.81.5 react: 19.1.0 - '@tanstack/react-router@1.121.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@tanstack/react-router@1.123.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: - '@tanstack/history': 1.120.17 + '@tanstack/history': 1.121.34 '@tanstack/react-store': 0.7.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@tanstack/router-core': 1.121.0 + '@tanstack/router-core': 1.123.2 + isbot: 5.1.28 jsesc: 3.1.0 react: 19.1.0 react-dom: 19.1.0(react@19.1.0) @@ -6087,54 +6092,57 @@ snapshots: react-dom: 19.1.0(react@19.1.0) use-sync-external-store: 1.5.0(react@19.1.0) - '@tanstack/router-core@1.121.0': + '@tanstack/router-core@1.123.2': dependencies: - '@tanstack/history': 1.120.17 + '@tanstack/history': 1.121.34 '@tanstack/store': 0.7.1 + cookie-es: 1.2.2 + jsesc: 3.1.0 tiny-invariant: 1.3.3 + tiny-warning: 1.0.3 - '@tanstack/router-generator@1.121.0': + '@tanstack/router-generator@1.123.2': dependencies: - '@tanstack/router-core': 1.121.0 - '@tanstack/router-utils': 1.121.0 - '@tanstack/virtual-file-routes': 1.120.17 + '@tanstack/router-core': 1.123.2 + '@tanstack/router-utils': 1.121.21 + '@tanstack/virtual-file-routes': 1.121.21 prettier: 3.5.3 recast: 0.23.11 source-map: 0.7.4 - tsx: 4.19.4 - zod: 3.25.58 + tsx: 4.20.3 + zod: 3.25.67 transitivePeerDependencies: - supports-color - '@tanstack/router-plugin@1.121.0(@tanstack/react-router@1.121.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(vite@6.3.5(@types/node@22.15.31)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0))(webpack@5.99.9(esbuild@0.25.5))': + '@tanstack/router-plugin@1.123.2(@tanstack/react-router@1.123.2(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.43.1)(tsx@4.20.3)(yaml@2.8.0))(webpack@5.99.9(esbuild@0.25.5))': dependencies: - '@babel/core': 7.27.4 - '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.27.4) - '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.27.4) + '@babel/core': 7.27.7 + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.27.7) + '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.27.7) '@babel/template': 7.27.2 - '@babel/traverse': 7.27.4 - '@babel/types': 7.27.6 - '@tanstack/router-core': 1.121.0 - '@tanstack/router-generator': 1.121.0 - '@tanstack/router-utils': 1.121.0 - '@tanstack/virtual-file-routes': 1.120.17 + '@babel/traverse': 7.27.7 + '@babel/types': 7.27.7 + '@tanstack/router-core': 1.123.2 + '@tanstack/router-generator': 1.123.2 + '@tanstack/router-utils': 1.121.21 + '@tanstack/virtual-file-routes': 1.121.21 babel-dead-code-elimination: 1.0.10 chokidar: 3.6.0 unplugin: 2.3.5 - zod: 3.25.58 + zod: 3.25.67 optionalDependencies: - '@tanstack/react-router': 1.121.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - vite: 6.3.5(@types/node@22.15.31)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0) + '@tanstack/react-router': 1.123.2(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.43.1)(tsx@4.20.3)(yaml@2.8.0) webpack: 5.99.9(esbuild@0.25.5) transitivePeerDependencies: - supports-color - '@tanstack/router-utils@1.121.0': + '@tanstack/router-utils@1.121.21': dependencies: - '@babel/core': 7.27.4 + '@babel/core': 7.27.7 '@babel/generator': 7.27.5 - '@babel/parser': 7.27.5 - '@babel/preset-typescript': 7.27.1(@babel/core@7.27.4) + '@babel/parser': 7.27.7 + '@babel/preset-typescript': 7.27.1(@babel/core@7.27.7) ansis: 4.1.0 diff: 8.0.2 transitivePeerDependencies: @@ -6142,7 +6150,7 @@ snapshots: '@tanstack/store@0.7.1': {} - '@tanstack/virtual-file-routes@1.120.17': {} + '@tanstack/virtual-file-routes@1.121.21': {} '@testing-library/dom@10.4.0': dependencies: @@ -6175,15 +6183,15 @@ snapshots: lodash: 4.17.21 redent: 3.0.0 - '@testing-library/react@16.3.0(@testing-library/dom@10.4.0)(@types/react-dom@19.1.6(@types/react@19.1.7))(@types/react@19.1.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@testing-library/react@16.3.0(@testing-library/dom@10.4.0)(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@babel/runtime': 7.27.6 '@testing-library/dom': 10.4.0 react: 19.1.0 react-dom: 19.1.0(react@19.1.0) optionalDependencies: - '@types/react': 19.1.7 - '@types/react-dom': 19.1.6(@types/react@19.1.7) + '@types/react': 19.1.8 + '@types/react-dom': 19.1.6(@types/react@19.1.8) '@testing-library/user-event@14.5.2(@testing-library/dom@10.4.0)': dependencies: @@ -6197,24 +6205,24 @@ snapshots: '@types/babel__core@7.20.5': dependencies: - '@babel/parser': 7.27.5 - '@babel/types': 7.27.6 + '@babel/parser': 7.27.7 + '@babel/types': 7.27.7 '@types/babel__generator': 7.27.0 '@types/babel__template': 7.4.4 '@types/babel__traverse': 7.20.7 '@types/babel__generator@7.27.0': dependencies: - '@babel/types': 7.27.6 + '@babel/types': 7.27.7 '@types/babel__template@7.4.4': dependencies: - '@babel/parser': 7.27.5 - '@babel/types': 7.27.6 + '@babel/parser': 7.27.7 + '@babel/types': 7.27.7 '@types/babel__traverse@7.20.7': dependencies: - '@babel/types': 7.27.6 + '@babel/types': 7.27.7 '@types/chai@5.2.2': dependencies: @@ -6234,8 +6242,6 @@ snapshots: '@types/estree': 1.0.8 '@types/json-schema': 7.0.15 - '@types/estree@1.0.7': {} - '@types/estree@1.0.8': {} '@types/js-cookie@3.0.6': {} @@ -6244,15 +6250,15 @@ snapshots: '@types/mdx@2.0.13': {} - '@types/node@22.15.31': + '@types/node@22.15.34': dependencies: undici-types: 6.21.0 - '@types/react-dom@19.1.6(@types/react@19.1.7)': + '@types/react-dom@19.1.6(@types/react@19.1.8)': dependencies: - '@types/react': 19.1.7 + '@types/react': 19.1.8 - '@types/react@19.1.7': + '@types/react@19.1.8': dependencies: csstype: 3.1.3 @@ -6260,15 +6266,15 @@ snapshots: '@types/uuid@9.0.8': {} - '@typescript-eslint/eslint-plugin@8.34.0(@typescript-eslint/parser@8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)': + '@typescript-eslint/eslint-plugin@8.35.1(@typescript-eslint/parser@8.35.1(eslint@9.30.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.30.0(jiti@2.4.2))(typescript@5.8.3)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/scope-manager': 8.34.0 - '@typescript-eslint/type-utils': 8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/utils': 8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/visitor-keys': 8.34.0 - eslint: 9.28.0(jiti@2.4.2) + '@typescript-eslint/parser': 8.35.1(eslint@9.30.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/scope-manager': 8.35.1 + '@typescript-eslint/type-utils': 8.35.1(eslint@9.30.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/utils': 8.35.1(eslint@9.30.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/visitor-keys': 8.35.1 + eslint: 9.30.0(jiti@2.4.2) graphemer: 1.4.0 ignore: 7.0.5 natural-compare: 1.4.0 @@ -6277,55 +6283,55 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)': + '@typescript-eslint/parser@8.35.1(eslint@9.30.0(jiti@2.4.2))(typescript@5.8.3)': dependencies: - '@typescript-eslint/scope-manager': 8.34.0 - '@typescript-eslint/types': 8.34.0 - '@typescript-eslint/typescript-estree': 8.34.0(typescript@5.8.3) - '@typescript-eslint/visitor-keys': 8.34.0 + '@typescript-eslint/scope-manager': 8.35.1 + '@typescript-eslint/types': 8.35.1 + '@typescript-eslint/typescript-estree': 8.35.1(typescript@5.8.3) + '@typescript-eslint/visitor-keys': 8.35.1 debug: 4.4.1 - eslint: 9.28.0(jiti@2.4.2) + eslint: 9.30.0(jiti@2.4.2) typescript: 5.8.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.34.0(typescript@5.8.3)': + '@typescript-eslint/project-service@8.35.1(typescript@5.8.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.34.0(typescript@5.8.3) - '@typescript-eslint/types': 8.34.0 + '@typescript-eslint/tsconfig-utils': 8.35.1(typescript@5.8.3) + '@typescript-eslint/types': 8.35.1 debug: 4.4.1 typescript: 5.8.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.34.0': + '@typescript-eslint/scope-manager@8.35.1': dependencies: - '@typescript-eslint/types': 8.34.0 - '@typescript-eslint/visitor-keys': 8.34.0 + '@typescript-eslint/types': 8.35.1 + '@typescript-eslint/visitor-keys': 8.35.1 - '@typescript-eslint/tsconfig-utils@8.34.0(typescript@5.8.3)': + '@typescript-eslint/tsconfig-utils@8.35.1(typescript@5.8.3)': dependencies: typescript: 5.8.3 - '@typescript-eslint/type-utils@8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)': + '@typescript-eslint/type-utils@8.35.1(eslint@9.30.0(jiti@2.4.2))(typescript@5.8.3)': dependencies: - '@typescript-eslint/typescript-estree': 8.34.0(typescript@5.8.3) - '@typescript-eslint/utils': 8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/typescript-estree': 8.35.1(typescript@5.8.3) + '@typescript-eslint/utils': 8.35.1(eslint@9.30.0(jiti@2.4.2))(typescript@5.8.3) debug: 4.4.1 - eslint: 9.28.0(jiti@2.4.2) + eslint: 9.30.0(jiti@2.4.2) ts-api-utils: 2.1.0(typescript@5.8.3) typescript: 5.8.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.34.0': {} + '@typescript-eslint/types@8.35.1': {} - '@typescript-eslint/typescript-estree@8.34.0(typescript@5.8.3)': + '@typescript-eslint/typescript-estree@8.35.1(typescript@5.8.3)': dependencies: - '@typescript-eslint/project-service': 8.34.0(typescript@5.8.3) - '@typescript-eslint/tsconfig-utils': 8.34.0(typescript@5.8.3) - '@typescript-eslint/types': 8.34.0 - '@typescript-eslint/visitor-keys': 8.34.0 + '@typescript-eslint/project-service': 8.35.1(typescript@5.8.3) + '@typescript-eslint/tsconfig-utils': 8.35.1(typescript@5.8.3) + '@typescript-eslint/types': 8.35.1 + '@typescript-eslint/visitor-keys': 8.35.1 debug: 4.4.1 fast-glob: 3.3.3 is-glob: 4.0.3 @@ -6336,54 +6342,54 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)': + '@typescript-eslint/utils@8.35.1(eslint@9.30.0(jiti@2.4.2))(typescript@5.8.3)': dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.28.0(jiti@2.4.2)) - '@typescript-eslint/scope-manager': 8.34.0 - '@typescript-eslint/types': 8.34.0 - '@typescript-eslint/typescript-estree': 8.34.0(typescript@5.8.3) - eslint: 9.28.0(jiti@2.4.2) + '@eslint-community/eslint-utils': 4.7.0(eslint@9.30.0(jiti@2.4.2)) + '@typescript-eslint/scope-manager': 8.35.1 + '@typescript-eslint/types': 8.35.1 + '@typescript-eslint/typescript-estree': 8.35.1(typescript@5.8.3) + eslint: 9.30.0(jiti@2.4.2) typescript: 5.8.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.34.0': + '@typescript-eslint/visitor-keys@8.35.1': dependencies: - '@typescript-eslint/types': 8.34.0 + '@typescript-eslint/types': 8.35.1 eslint-visitor-keys: 4.2.1 - '@vitejs/plugin-react@4.5.2(vite@6.3.5(@types/node@22.15.31)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(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.43.1)(tsx@4.20.3)(yaml@2.8.0))': dependencies: - '@babel/core': 7.27.4 - '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.27.4) - '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.27.4) - '@rolldown/pluginutils': 1.0.0-beta.11 + '@babel/core': 7.27.7 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.27.7) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.27.7) + '@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.31)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) transitivePeerDependencies: - supports-color - '@vitest/browser@3.2.3(playwright@1.53.0)(vite@6.3.5(@types/node@22.15.31)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0))(vitest@3.2.3)': + '@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.43.1)(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.3(vite@6.3.5(@types/node@22.15.31)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0)) - '@vitest/utils': 3.2.3 + '@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(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.3(@types/node@22.15.31)(@vitest/browser@3.2.3)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0) - ws: 8.18.2 + 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.43.1)(tsx@4.20.3)(yaml@2.8.0) + ws: 8.18.3 optionalDependencies: - playwright: 1.53.0 + playwright: 1.53.2 transitivePeerDependencies: - bufferutil - msw - utf-8-validate - vite - '@vitest/coverage-v8@3.2.3(@vitest/browser@3.2.3)(vitest@3.2.3)': + '@vitest/coverage-v8@3.2.4(@vitest/browser@3.2.4)(vitest@3.2.4)': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 @@ -6398,9 +6404,9 @@ snapshots: std-env: 3.9.0 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.2.3(@types/node@22.15.31)(@vitest/browser@3.2.3)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(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.43.1)(tsx@4.20.3)(yaml@2.8.0) optionalDependencies: - '@vitest/browser': 3.2.3(playwright@1.53.0)(vite@6.3.5(@types/node@22.15.31)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0))(vitest@3.2.3) + '@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.43.1)(tsx@4.20.3)(yaml@2.8.0))(vitest@3.2.4) transitivePeerDependencies: - supports-color @@ -6411,21 +6417,21 @@ snapshots: chai: 5.2.0 tinyrainbow: 1.2.0 - '@vitest/expect@3.2.3': + '@vitest/expect@3.2.4': dependencies: '@types/chai': 5.2.2 - '@vitest/spy': 3.2.3 - '@vitest/utils': 3.2.3 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 chai: 5.2.0 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.3(vite@6.3.5(@types/node@22.15.31)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(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.43.1)(tsx@4.20.3)(yaml@2.8.0))': dependencies: - '@vitest/spy': 3.2.3 + '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: 6.3.5(@types/node@22.15.31)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) '@vitest/pretty-format@2.0.5': dependencies: @@ -6435,19 +6441,19 @@ snapshots: dependencies: tinyrainbow: 1.2.0 - '@vitest/pretty-format@3.2.3': + '@vitest/pretty-format@3.2.4': dependencies: tinyrainbow: 2.0.0 - '@vitest/runner@3.2.3': + '@vitest/runner@3.2.4': dependencies: - '@vitest/utils': 3.2.3 + '@vitest/utils': 3.2.4 pathe: 2.0.3 strip-literal: 3.0.0 - '@vitest/snapshot@3.2.3': + '@vitest/snapshot@3.2.4': dependencies: - '@vitest/pretty-format': 3.2.3 + '@vitest/pretty-format': 3.2.4 magic-string: 0.30.17 pathe: 2.0.3 @@ -6455,7 +6461,7 @@ snapshots: dependencies: tinyspy: 3.0.2 - '@vitest/spy@3.2.3': + '@vitest/spy@3.2.4': dependencies: tinyspy: 4.0.3 @@ -6463,19 +6469,19 @@ snapshots: dependencies: '@vitest/pretty-format': 2.0.5 estree-walker: 3.0.3 - loupe: 3.1.3 + loupe: 3.1.4 tinyrainbow: 1.2.0 '@vitest/utils@2.1.9': dependencies: '@vitest/pretty-format': 2.1.9 - loupe: 3.1.3 + loupe: 3.1.4 tinyrainbow: 1.2.0 - '@vitest/utils@3.2.3': + '@vitest/utils@3.2.4': dependencies: - '@vitest/pretty-format': 3.2.3 - loupe: 3.1.3 + '@vitest/pretty-format': 3.2.4 + loupe: 3.1.4 tinyrainbow: 2.0.0 '@webassemblyjs/ast@1.14.1': @@ -6628,7 +6634,7 @@ snapshots: ast-v8-to-istanbul@0.3.3: dependencies: - '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/trace-mapping': 0.3.28 estree-walker: 3.0.3 js-tokens: 9.0.1 @@ -6638,7 +6644,7 @@ snapshots: dependencies: possible-typed-array-names: 1.1.0 - axios@1.9.0: + axios@1.10.0: dependencies: follow-redirects: 1.15.9 form-data: 4.0.3 @@ -6648,10 +6654,10 @@ snapshots: babel-dead-code-elimination@1.0.10: dependencies: - '@babel/core': 7.27.4 - '@babel/parser': 7.27.5 - '@babel/traverse': 7.27.4 - '@babel/types': 7.27.6 + '@babel/core': 7.27.7 + '@babel/parser': 7.27.7 + '@babel/traverse': 7.27.7 + '@babel/types': 7.27.7 transitivePeerDependencies: - supports-color @@ -6663,12 +6669,12 @@ snapshots: binary-extensions@2.3.0: {} - brace-expansion@1.1.11: + brace-expansion@1.1.12: dependencies: balanced-match: 1.0.2 concat-map: 0.0.1 - brace-expansion@2.0.1: + brace-expansion@2.0.2: dependencies: balanced-match: 1.0.2 @@ -6678,12 +6684,12 @@ snapshots: browser-assert@1.2.1: {} - browserslist@4.25.0: + browserslist@4.25.1: dependencies: - caniuse-lite: 1.0.30001721 - electron-to-chromium: 1.5.166 + caniuse-lite: 1.0.30001726 + electron-to-chromium: 1.5.178 node-releases: 2.0.19 - update-browserslist-db: 1.1.3(browserslist@4.25.0) + update-browserslist-db: 1.1.3(browserslist@4.25.1) buffer-from@1.1.2: {} @@ -6710,15 +6716,15 @@ snapshots: camelcase@6.3.0: {} - caniuse-lite@1.0.30001721: {} + caniuse-lite@1.0.30001726: {} chai@5.2.0: dependencies: assertion-error: 2.0.1 check-error: 2.1.1 deep-eql: 5.0.2 - loupe: 3.1.3 - pathval: 2.0.0 + loupe: 3.1.4 + pathval: 2.0.1 chalk@3.0.0: dependencies: @@ -6789,6 +6795,8 @@ snapshots: convert-source-map@2.0.0: {} + cookie-es@1.2.2: {} + cosmiconfig@8.3.6(typescript@5.8.3): dependencies: import-fresh: 3.3.1 @@ -6806,7 +6814,7 @@ snapshots: css.escape@1.5.1: {} - cssstyle@4.4.0: + cssstyle@4.6.0: dependencies: '@asamuzakjp/css-color': 3.2.0 rrweb-cssom: 0.8.0 @@ -6865,7 +6873,7 @@ snapshots: eastasianwidth@0.2.0: {} - electron-to-chromium@1.5.166: {} + electron-to-chromium@1.5.178: {} emoji-regex@10.4.0: {} @@ -6873,7 +6881,7 @@ snapshots: emoji-regex@9.2.2: {} - enhanced-resolve@5.18.1: + enhanced-resolve@5.18.2: dependencies: graceful-fs: 4.2.11 tapable: 2.2.2 @@ -6944,19 +6952,19 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-plugin-react-hooks@5.2.0(eslint@9.28.0(jiti@2.4.2)): + eslint-plugin-react-hooks@5.2.0(eslint@9.30.0(jiti@2.4.2)): dependencies: - eslint: 9.28.0(jiti@2.4.2) + eslint: 9.30.0(jiti@2.4.2) - eslint-plugin-react-refresh@0.4.20(eslint@9.28.0(jiti@2.4.2)): + eslint-plugin-react-refresh@0.4.20(eslint@9.30.0(jiti@2.4.2)): dependencies: - eslint: 9.28.0(jiti@2.4.2) + eslint: 9.30.0(jiti@2.4.2) - eslint-plugin-storybook@0.12.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3): + eslint-plugin-storybook@0.12.0(eslint@9.30.0(jiti@2.4.2))(typescript@5.8.3): dependencies: '@storybook/csf': 0.1.13 - '@typescript-eslint/utils': 8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - eslint: 9.28.0(jiti@2.4.2) + '@typescript-eslint/utils': 8.35.1(eslint@9.30.0(jiti@2.4.2))(typescript@5.8.3) + eslint: 9.30.0(jiti@2.4.2) ts-dedent: 2.2.0 transitivePeerDependencies: - supports-color @@ -6976,16 +6984,16 @@ snapshots: eslint-visitor-keys@4.2.1: {} - eslint@9.28.0(jiti@2.4.2): + eslint@9.30.0(jiti@2.4.2): dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.28.0(jiti@2.4.2)) + '@eslint-community/eslint-utils': 4.7.0(eslint@9.30.0(jiti@2.4.2)) '@eslint-community/regexpp': 4.12.1 - '@eslint/config-array': 0.20.0 - '@eslint/config-helpers': 0.2.2 + '@eslint/config-array': 0.21.0 + '@eslint/config-helpers': 0.3.0 '@eslint/core': 0.14.0 '@eslint/eslintrc': 3.3.1 - '@eslint/js': 9.28.0 - '@eslint/plugin-kit': 0.3.1 + '@eslint/js': 9.30.0 + '@eslint/plugin-kit': 0.3.3 '@humanfs/node': 0.16.6 '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.4.3 @@ -7327,6 +7335,8 @@ snapshots: dependencies: is-docker: 2.2.1 + isbot@5.1.28: {} + isexe@2.0.0: {} istanbul-lib-coverage@3.2.2: {} @@ -7339,7 +7349,7 @@ snapshots: istanbul-lib-source-maps@5.0.6: dependencies: - '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/trace-mapping': 0.3.28 debug: 4.4.1 istanbul-lib-coverage: 3.2.2 transitivePeerDependencies: @@ -7358,7 +7368,7 @@ snapshots: jest-worker@27.5.1: dependencies: - '@types/node': 22.15.31 + '@types/node': 22.15.34 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -7378,7 +7388,7 @@ snapshots: jsdom@26.1.0: dependencies: - cssstyle: 4.4.0 + cssstyle: 4.6.0 data-urls: 5.0.0 decimal.js: 10.5.0 html-encoding-sniffer: 4.0.0 @@ -7396,7 +7406,7 @@ snapshots: whatwg-encoding: 3.1.1 whatwg-mimetype: 4.0.0 whatwg-url: 14.2.0 - ws: 8.18.2 + ws: 8.18.3 xml-name-validator: 5.0.0 transitivePeerDependencies: - bufferutil @@ -7516,7 +7526,7 @@ snapshots: local-pkg@1.1.1: dependencies: mlly: 1.7.4 - pkg-types: 2.1.1 + pkg-types: 2.2.0 quansync: 0.2.10 locate-path@6.0.0: @@ -7535,7 +7545,7 @@ snapshots: strip-ansi: 7.1.0 wrap-ansi: 9.0.0 - loupe@3.1.3: {} + loupe@3.1.4: {} lower-case@2.0.2: dependencies: @@ -7551,16 +7561,16 @@ snapshots: magic-string@0.27.0: dependencies: - '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/sourcemap-codec': 1.5.3 magic-string@0.30.17: dependencies: - '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/sourcemap-codec': 1.5.3 magicast@0.3.5: dependencies: - '@babel/parser': 7.27.5 - '@babel/types': 7.27.6 + '@babel/parser': 7.27.7 + '@babel/types': 7.27.7 source-map-js: 1.2.1 make-dir@4.0.0: @@ -7598,11 +7608,11 @@ snapshots: minimatch@3.1.2: dependencies: - brace-expansion: 1.1.11 + brace-expansion: 1.1.12 minimatch@9.0.5: dependencies: - brace-expansion: 2.0.1 + brace-expansion: 2.0.2 minimist@1.2.8: {} @@ -7713,7 +7723,7 @@ snapshots: pathe@2.0.3: {} - pathval@2.0.0: {} + pathval@2.0.1: {} picocolors@1.1.1: {} @@ -7729,17 +7739,17 @@ snapshots: mlly: 1.7.4 pathe: 2.0.3 - pkg-types@2.1.1: + pkg-types@2.2.0: dependencies: confbox: 0.2.2 exsolve: 1.0.7 pathe: 2.0.3 - playwright-core@1.53.0: {} + playwright-core@1.53.2: {} - playwright@1.53.0: + playwright@1.53.2: dependencies: - playwright-core: 1.53.0 + playwright-core: 1.53.2 optionalDependencies: fsevents: 2.3.2 @@ -7749,7 +7759,7 @@ snapshots: possible-typed-array-names@1.1.0: {} - postcss@8.5.4: + postcss@8.5.6: dependencies: nanoid: 3.3.11 picocolors: 1.1.1 @@ -7875,9 +7885,9 @@ snapshots: react-docgen@7.1.1: dependencies: - '@babel/core': 7.27.4 - '@babel/traverse': 7.27.4 - '@babel/types': 7.27.6 + '@babel/core': 7.27.7 + '@babel/traverse': 7.27.7 + '@babel/types': 7.27.7 '@types/babel__core': 7.20.5 '@types/babel__traverse': 7.20.7 '@types/doctrine': 0.0.9 @@ -7967,30 +7977,30 @@ snapshots: rfdc@1.4.1: {} - rollup@4.42.0: + rollup@4.44.1: dependencies: - '@types/estree': 1.0.7 + '@types/estree': 1.0.8 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.42.0 - '@rollup/rollup-android-arm64': 4.42.0 - '@rollup/rollup-darwin-arm64': 4.42.0 - '@rollup/rollup-darwin-x64': 4.42.0 - '@rollup/rollup-freebsd-arm64': 4.42.0 - '@rollup/rollup-freebsd-x64': 4.42.0 - '@rollup/rollup-linux-arm-gnueabihf': 4.42.0 - '@rollup/rollup-linux-arm-musleabihf': 4.42.0 - '@rollup/rollup-linux-arm64-gnu': 4.42.0 - '@rollup/rollup-linux-arm64-musl': 4.42.0 - '@rollup/rollup-linux-loongarch64-gnu': 4.42.0 - '@rollup/rollup-linux-powerpc64le-gnu': 4.42.0 - '@rollup/rollup-linux-riscv64-gnu': 4.42.0 - '@rollup/rollup-linux-riscv64-musl': 4.42.0 - '@rollup/rollup-linux-s390x-gnu': 4.42.0 - '@rollup/rollup-linux-x64-gnu': 4.42.0 - '@rollup/rollup-linux-x64-musl': 4.42.0 - '@rollup/rollup-win32-arm64-msvc': 4.42.0 - '@rollup/rollup-win32-ia32-msvc': 4.42.0 - '@rollup/rollup-win32-x64-msvc': 4.42.0 + '@rollup/rollup-android-arm-eabi': 4.44.1 + '@rollup/rollup-android-arm64': 4.44.1 + '@rollup/rollup-darwin-arm64': 4.44.1 + '@rollup/rollup-darwin-x64': 4.44.1 + '@rollup/rollup-freebsd-arm64': 4.44.1 + '@rollup/rollup-freebsd-x64': 4.44.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.44.1 + '@rollup/rollup-linux-arm-musleabihf': 4.44.1 + '@rollup/rollup-linux-arm64-gnu': 4.44.1 + '@rollup/rollup-linux-arm64-musl': 4.44.1 + '@rollup/rollup-linux-loongarch64-gnu': 4.44.1 + '@rollup/rollup-linux-powerpc64le-gnu': 4.44.1 + '@rollup/rollup-linux-riscv64-gnu': 4.44.1 + '@rollup/rollup-linux-riscv64-musl': 4.44.1 + '@rollup/rollup-linux-s390x-gnu': 4.44.1 + '@rollup/rollup-linux-x64-gnu': 4.44.1 + '@rollup/rollup-linux-x64-musl': 4.44.1 + '@rollup/rollup-win32-arm64-msvc': 4.44.1 + '@rollup/rollup-win32-ia32-msvc': 4.44.1 + '@rollup/rollup-win32-x64-msvc': 4.44.1 fsevents: 2.3.3 rrweb-cssom@0.8.0: {} @@ -8161,16 +8171,16 @@ snapshots: tailwind-merge@3.3.1: {} - tailwind-variants@1.0.0(tailwindcss@4.1.8): + tailwind-variants@1.0.0(tailwindcss@4.1.11): dependencies: tailwind-merge: 3.0.2 - tailwindcss: 4.1.8 + tailwindcss: 4.1.11 - tailwindcss-react-aria-components@2.0.0(tailwindcss@4.1.8): + tailwindcss-react-aria-components@2.0.0(tailwindcss@4.1.11): dependencies: - tailwindcss: 4.1.8 + tailwindcss: 4.1.11 - tailwindcss@4.1.8: {} + tailwindcss@4.1.11: {} tapable@2.2.2: {} @@ -8185,18 +8195,18 @@ snapshots: terser-webpack-plugin@5.3.14(esbuild@0.25.5)(webpack@5.99.9(esbuild@0.25.5)): dependencies: - '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/trace-mapping': 0.3.28 jest-worker: 27.5.1 schema-utils: 4.3.2 serialize-javascript: 6.0.2 - terser: 5.42.0 + terser: 5.43.1 webpack: 5.99.9(esbuild@0.25.5) optionalDependencies: esbuild: 0.25.5 - terser@5.42.0: + terser@5.43.1: dependencies: - '@jridgewell/source-map': 0.3.6 + '@jridgewell/source-map': 0.3.9 acorn: 8.15.0 commander: 2.20.3 source-map-support: 0.5.21 @@ -8222,7 +8232,7 @@ snapshots: fdir: 6.4.6(picomatch@4.0.2) picomatch: 4.0.2 - tinypool@1.1.0: {} + tinypool@1.1.1: {} tinyrainbow@1.2.0: {} @@ -8266,7 +8276,7 @@ snapshots: tslib@2.8.1: {} - tsx@4.19.4: + tsx@4.20.3: dependencies: esbuild: 0.25.5 get-tsconfig: 4.10.1 @@ -8281,12 +8291,12 @@ snapshots: type-fest@2.19.0: {} - typescript-eslint@8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3): + typescript-eslint@8.35.1(eslint@9.30.0(jiti@2.4.2))(typescript@5.8.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.34.0(@typescript-eslint/parser@8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/parser': 8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/utils': 8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - eslint: 9.28.0(jiti@2.4.2) + '@typescript-eslint/eslint-plugin': 8.35.1(@typescript-eslint/parser@8.35.1(eslint@9.30.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.30.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/parser': 8.35.1(eslint@9.30.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/utils': 8.35.1(eslint@9.30.0(jiti@2.4.2))(typescript@5.8.3) + eslint: 9.30.0(jiti@2.4.2) typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -8322,9 +8332,9 @@ snapshots: picomatch: 4.0.2 webpack-virtual-modules: 0.6.2 - update-browserslist-db@1.1.3(browserslist@4.25.0): + update-browserslist-db@1.1.3(browserslist@4.25.1): dependencies: - browserslist: 4.25.0 + browserslist: 4.25.1 escalade: 3.2.0 picocolors: 1.1.1 @@ -8346,13 +8356,13 @@ snapshots: uuid@9.0.1: {} - vite-node@3.2.3(@types/node@22.15.31)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0): + vite-node@3.2.4(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0): dependencies: cac: 6.7.14 debug: 4.4.1 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 6.3.5(@types/node@22.15.31)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) transitivePeerDependencies: - '@types/node' - jiti @@ -8367,33 +8377,33 @@ snapshots: - tsx - yaml - vite@6.3.5(@types/node@22.15.31)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0): + vite@6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0): dependencies: esbuild: 0.25.5 fdir: 6.4.6(picomatch@4.0.2) picomatch: 4.0.2 - postcss: 8.5.4 - rollup: 4.42.0 + postcss: 8.5.6 + rollup: 4.44.1 tinyglobby: 0.2.14 optionalDependencies: - '@types/node': 22.15.31 + '@types/node': 22.15.34 fsevents: 2.3.3 jiti: 2.4.2 lightningcss: 1.30.1 - terser: 5.42.0 - tsx: 4.19.4 + terser: 5.43.1 + tsx: 4.20.3 yaml: 2.8.0 - vitest@3.2.3(@types/node@22.15.31)(@vitest/browser@3.2.3)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(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.43.1)(tsx@4.20.3)(yaml@2.8.0): dependencies: '@types/chai': 5.2.2 - '@vitest/expect': 3.2.3 - '@vitest/mocker': 3.2.3(vite@6.3.5(@types/node@22.15.31)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0)) - '@vitest/pretty-format': 3.2.3 - '@vitest/runner': 3.2.3 - '@vitest/snapshot': 3.2.3 - '@vitest/spy': 3.2.3 - '@vitest/utils': 3.2.3 + '@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.43.1)(tsx@4.20.3)(yaml@2.8.0)) + '@vitest/pretty-format': 3.2.4 + '@vitest/runner': 3.2.4 + '@vitest/snapshot': 3.2.4 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 chai: 5.2.0 debug: 4.4.1 expect-type: 1.2.1 @@ -8404,14 +8414,14 @@ snapshots: tinybench: 2.9.0 tinyexec: 0.3.2 tinyglobby: 0.2.14 - tinypool: 1.1.0 + tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 6.3.5(@types/node@22.15.31)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0) - vite-node: 3.2.3(@types/node@22.15.31)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.5(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.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.43.1)(tsx@4.20.3)(yaml@2.8.0) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 22.15.31 - '@vitest/browser': 3.2.3(playwright@1.53.0)(vite@6.3.5(@types/node@22.15.31)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0))(vitest@3.2.3) + '@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.43.1)(tsx@4.20.3)(yaml@2.8.0))(vitest@3.2.4) jsdom: 26.1.0 transitivePeerDependencies: - jiti @@ -8438,7 +8448,7 @@ snapshots: webidl-conversions@7.0.0: {} - webpack-sources@3.3.2: {} + webpack-sources@3.3.3: {} webpack-virtual-modules@0.6.2: {} @@ -8451,9 +8461,9 @@ snapshots: '@webassemblyjs/wasm-edit': 1.14.1 '@webassemblyjs/wasm-parser': 1.14.1 acorn: 8.15.0 - browserslist: 4.25.0 + browserslist: 4.25.1 chrome-trace-event: 1.0.4 - enhanced-resolve: 5.18.1 + enhanced-resolve: 5.18.2 es-module-lexer: 1.7.0 eslint-scope: 5.1.1 events: 3.3.0 @@ -8467,7 +8477,7 @@ snapshots: tapable: 2.2.2 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.2 + webpack-sources: 3.3.3 transitivePeerDependencies: - '@swc/core' - esbuild @@ -8523,7 +8533,7 @@ snapshots: string-width: 7.2.0 strip-ansi: 7.1.0 - ws@8.18.2: {} + ws@8.18.3: {} xml-name-validator@5.0.0: {} @@ -8537,4 +8547,4 @@ snapshots: yocto-queue@0.1.0: {} - zod@3.25.58: {} + zod@3.25.67: {} diff --git a/docker-compose.yml b/docker-compose.yml index e3d78491..292d7adb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,11 +15,10 @@ services: build: context: ./apps/web dockerfile: Dockerfile.dev - ports: - - "5173:5173" volumes: - ./apps/web:/app - - /app/node_modules + ports: + - "5173:5173" environment: - NODE_ENV=development depends_on: From 12552813e568c759e07f3ee6f2e3213af7019c57 Mon Sep 17 00:00:00 2001 From: Alexander Wang <98280966+AlexanderWangY@users.noreply.github.com> Date: Mon, 30 Jun 2025 23:48:54 -0400 Subject: [PATCH 08/52] feat: added openapi-typescript for type gen (#42) --- apps/web/package.json | 4 +- apps/web/pnpm-lock.yaml | 178 ++++++++++++++++++--- apps/web/src/lib/openapi/schema.d.ts | 229 +++++++++++++++++++++++++++ apps/web/tsconfig.json | 13 +- shared/openapi/swagger.yaml | 62 ++------ 5 files changed, 415 insertions(+), 71 deletions(-) create mode 100644 apps/web/src/lib/openapi/schema.d.ts diff --git a/apps/web/package.json b/apps/web/package.json index 0a04b0ab..a53ac09c 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -13,7 +13,8 @@ "prepare": "cd ../.. && husky ./apps/web/.husky", "format": "prettier --write ./src && git add --all", "storybook": "storybook dev -p 6006", - "build-storybook": "storybook build" + "build-storybook": "storybook build", + "generate:openapi": "openapi-typescript ../../shared/openapi/swagger.yaml -o ./src/lib/openapi/schema.d.ts" }, "lint-staged": { "*.{js,jsx,ts,tsx}": [ @@ -74,6 +75,7 @@ "husky": "^9.1.7", "jsdom": "^26.1.0", "lint-staged": "^15.5.2", + "openapi-typescript": "^7.8.0", "playwright": "^1.52.0", "prettier": "3.5.3", "storybook": "^8.6.12", diff --git a/apps/web/pnpm-lock.yaml b/apps/web/pnpm-lock.yaml index 095afabd..ccc42f3f 100644 --- a/apps/web/pnpm-lock.yaml +++ b/apps/web/pnpm-lock.yaml @@ -150,6 +150,9 @@ importers: lint-staged: specifier: ^15.5.2 version: 15.5.2 + openapi-typescript: + specifier: ^7.8.0 + version: 7.8.0(typescript@5.8.3) playwright: specifier: ^1.52.0 version: 1.53.2 @@ -1271,6 +1274,16 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + '@redocly/ajv@8.11.2': + resolution: {integrity: sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==} + + '@redocly/config@0.22.2': + resolution: {integrity: sha512-roRDai8/zr2S9YfmzUfNhKjOF0NdcOIqF7bhf4MVC5UxpjIysDjyudvlAiVbpPHp3eDRWbdzUgtkK1a7YiDNyQ==} + + '@redocly/openapi-core@1.34.3': + resolution: {integrity: sha512-3arRdUp1fNx55itnjKiUhO6t4Mf91TsrTIYINDNLAZPS0TPd5YpiXRctwjel0qqWoOOhjA34cZ3m4dksLDFUYg==} + engines: {node: '>=18.17.0', npm: '>=9.5.0'} + '@rolldown/pluginutils@1.0.0-beta.19': resolution: {integrity: sha512-3FL3mnMbPu0muGOCaKAhhFEYmqv9eTfPSJRJmANrCwtgK8VuxpsZDGK+m0LYAGoyO8+0j5uRe4PeyPDK1yA/hA==} @@ -2116,6 +2129,10 @@ packages: ajv@8.17.1: resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + ansi-escapes@7.0.0: resolution: {integrity: sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==} engines: {node: '>=18'} @@ -2257,6 +2274,9 @@ packages: resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + change-case@5.4.4: + resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==} + check-error@2.1.1: resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} engines: {node: '>= 16'} @@ -2307,6 +2327,9 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + colorette@1.4.0: + resolution: {integrity: sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==} + colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} @@ -2805,6 +2828,10 @@ packages: resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} engines: {node: '>=8'} + index-to-position@1.1.0: + resolution: {integrity: sha512-XPdx9Dq4t9Qk1mTMbWONJqU7boCoumEH7fRET37HX5+khDUl3J2W6PdALxhILYlIYx2amlwYcRPp28p0tSiojg==} + engines: {node: '>=18'} + inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} @@ -2920,6 +2947,10 @@ packages: resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==} engines: {node: '>=14'} + js-levenshtein@1.1.6: + resolution: {integrity: sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==} + engines: {node: '>=0.10.0'} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -3165,6 +3196,10 @@ packages: minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + minimatch@5.1.6: + resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} + engines: {node: '>=10'} + minimatch@9.0.5: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} @@ -3235,6 +3270,12 @@ packages: resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} engines: {node: '>=12'} + openapi-typescript@7.8.0: + resolution: {integrity: sha512-1EeVWmDzi16A+siQlo/SwSGIT7HwaFAVjvMA7/jG5HMLSnrUOzPL7uSTRZZa4v/LCRxHTApHKtNY6glApEoiUQ==} + hasBin: true + peerDependencies: + typescript: ^5.x + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -3261,6 +3302,10 @@ packages: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} + parse-json@8.3.0: + resolution: {integrity: sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==} + engines: {node: '>=18'} + parse5@7.3.0: resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} @@ -3326,6 +3371,10 @@ packages: engines: {node: '>=18'} hasBin: true + pluralize@8.0.0: + resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} + engines: {node: '>=4'} + polished@4.3.1: resolution: {integrity: sha512-OBatVyC/N7SCW/FaDHrSd+vn0o5cS855TOmYi4OkdWUMSJCET/xip//ch8xGUvtr3i44X9LVyWwQlRMTN3pwSA==} engines: {node: '>=10'} @@ -3620,6 +3669,10 @@ packages: strip-literal@3.0.0: resolution: {integrity: sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==} + supports-color@10.0.0: + resolution: {integrity: sha512-HRVVSbCCMbj7/kdWF9Q+bbckjBHLtHMEoJWlkmYzzdwhYMkjkOwubLM6t7NbWKjgKamGDrWL1++KrjUO1t9oAQ==} + engines: {node: '>=18'} + supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -3786,6 +3839,10 @@ packages: resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} engines: {node: '>=12.20'} + type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} + typescript-eslint@8.35.1: resolution: {integrity: sha512-xslJjFzhOmHYQzSB/QTeASAHbjmxOGEP6Coh93TXmUBFQoJ1VU35UHIDmG06Jd6taf3wqqC1ntBnCMeymy5Ovw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -3845,6 +3902,9 @@ packages: peerDependencies: browserslist: '>= 4.21.0' + uri-js-replace@1.0.1: + resolution: {integrity: sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==} + uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} @@ -4030,11 +4090,18 @@ packages: resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} engines: {node: '>=18'} + yaml-ast-parser@0.0.43: + resolution: {integrity: sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==} + yaml@2.8.0: resolution: {integrity: sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==} engines: {node: '>= 14.6'} hasBin: true + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -4087,7 +4154,7 @@ snapshots: '@babel/traverse': 7.27.7 '@babel/types': 7.27.7 convert-source-map: 2.0.0 - debug: 4.4.1 + debug: 4.4.1(supports-color@10.0.0) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -4252,7 +4319,7 @@ snapshots: '@babel/parser': 7.27.7 '@babel/template': 7.27.2 '@babel/types': 7.27.7 - debug: 4.4.1 + debug: 4.4.1(supports-color@10.0.0) globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -4382,7 +4449,7 @@ snapshots: '@eslint/config-array@0.21.0': dependencies: '@eslint/object-schema': 2.1.6 - debug: 4.4.1 + debug: 4.4.1(supports-color@10.0.0) minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -4400,7 +4467,7 @@ snapshots: '@eslint/eslintrc@3.3.1': dependencies: ajv: 6.12.6 - debug: 4.4.1 + debug: 4.4.1(supports-color@10.0.0) espree: 10.4.0 globals: 14.0.0 ignore: 5.3.2 @@ -4474,7 +4541,7 @@ snapshots: '@antfu/install-pkg': 1.1.0 '@antfu/utils': 8.1.1 '@iconify/types': 2.0.0 - debug: 4.4.1 + debug: 4.4.1(supports-color@10.0.0) globals: 15.15.0 kolorist: 1.8.0 local-pkg: 1.1.1 @@ -5608,6 +5675,29 @@ snapshots: '@react-types/shared': 3.30.0(react@19.1.0) react: 19.1.0 + '@redocly/ajv@8.11.2': + dependencies: + fast-deep-equal: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + uri-js-replace: 1.0.1 + + '@redocly/config@0.22.2': {} + + '@redocly/openapi-core@1.34.3(supports-color@10.0.0)': + dependencies: + '@redocly/ajv': 8.11.2 + '@redocly/config': 0.22.2 + colorette: 1.4.0 + https-proxy-agent: 7.0.6(supports-color@10.0.0) + js-levenshtein: 1.1.6 + js-yaml: 4.1.0 + minimatch: 5.1.6 + pluralize: 8.0.0 + yaml-ast-parser: 0.0.43 + transitivePeerDependencies: + - supports-color + '@rolldown/pluginutils@1.0.0-beta.19': {} '@rollup/pluginutils@5.2.0(rollup@4.44.1)': @@ -6289,7 +6379,7 @@ snapshots: '@typescript-eslint/types': 8.35.1 '@typescript-eslint/typescript-estree': 8.35.1(typescript@5.8.3) '@typescript-eslint/visitor-keys': 8.35.1 - debug: 4.4.1 + debug: 4.4.1(supports-color@10.0.0) eslint: 9.30.0(jiti@2.4.2) typescript: 5.8.3 transitivePeerDependencies: @@ -6299,7 +6389,7 @@ snapshots: dependencies: '@typescript-eslint/tsconfig-utils': 8.35.1(typescript@5.8.3) '@typescript-eslint/types': 8.35.1 - debug: 4.4.1 + debug: 4.4.1(supports-color@10.0.0) typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -6317,7 +6407,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 8.35.1(typescript@5.8.3) '@typescript-eslint/utils': 8.35.1(eslint@9.30.0(jiti@2.4.2))(typescript@5.8.3) - debug: 4.4.1 + debug: 4.4.1(supports-color@10.0.0) eslint: 9.30.0(jiti@2.4.2) ts-api-utils: 2.1.0(typescript@5.8.3) typescript: 5.8.3 @@ -6332,7 +6422,7 @@ snapshots: '@typescript-eslint/tsconfig-utils': 8.35.1(typescript@5.8.3) '@typescript-eslint/types': 8.35.1 '@typescript-eslint/visitor-keys': 8.35.1 - debug: 4.4.1 + debug: 4.4.1(supports-color@10.0.0) fast-glob: 3.3.3 is-glob: 4.0.3 minimatch: 9.0.5 @@ -6394,7 +6484,7 @@ snapshots: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 ast-v8-to-istanbul: 0.3.3 - debug: 4.4.1 + debug: 4.4.1(supports-color@10.0.0) istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 istanbul-lib-source-maps: 5.0.6 @@ -6595,6 +6685,8 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 + ansi-colors@4.1.3: {} + ansi-escapes@7.0.0: dependencies: environment: 1.1.0 @@ -6738,6 +6830,8 @@ snapshots: chalk@5.4.1: {} + change-case@5.4.4: {} + check-error@2.1.1: {} chokidar@3.6.0: @@ -6777,6 +6871,8 @@ snapshots: color-name@1.1.4: {} + colorette@1.4.0: {} + colorette@2.0.20: {} combined-stream@1.0.8: @@ -6826,9 +6922,11 @@ snapshots: whatwg-mimetype: 4.0.0 whatwg-url: 14.2.0 - debug@4.4.1: + debug@4.4.1(supports-color@10.0.0): dependencies: ms: 2.1.3 + optionalDependencies: + supports-color: 10.0.0 decimal.js@10.5.0: {} @@ -6915,7 +7013,7 @@ snapshots: esbuild-register@3.6.0(esbuild@0.25.5): dependencies: - debug: 4.4.1 + debug: 4.4.1(supports-color@10.0.0) esbuild: 0.25.5 transitivePeerDependencies: - supports-color @@ -7002,7 +7100,7 @@ snapshots: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.1 + debug: 4.4.1(supports-color@10.0.0) escape-string-regexp: 4.0.0 eslint-scope: 8.4.0 eslint-visitor-keys: 4.2.1 @@ -7233,14 +7331,14 @@ snapshots: http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.3 - debug: 4.4.1 + debug: 4.4.1(supports-color@10.0.0) transitivePeerDependencies: - supports-color - https-proxy-agent@7.0.6: + https-proxy-agent@7.0.6(supports-color@10.0.0): dependencies: agent-base: 7.1.3 - debug: 4.4.1 + debug: 4.4.1(supports-color@10.0.0) transitivePeerDependencies: - supports-color @@ -7265,6 +7363,8 @@ snapshots: indent-string@4.0.0: {} + index-to-position@1.1.0: {} + inherits@2.0.4: {} intl-messageformat@10.7.16: @@ -7350,7 +7450,7 @@ snapshots: istanbul-lib-source-maps@5.0.6: dependencies: '@jridgewell/trace-mapping': 0.3.28 - debug: 4.4.1 + debug: 4.4.1(supports-color@10.0.0) istanbul-lib-coverage: 3.2.2 transitivePeerDependencies: - supports-color @@ -7376,6 +7476,8 @@ snapshots: js-cookie@3.0.5: {} + js-levenshtein@1.1.6: {} + js-tokens@4.0.0: {} js-tokens@9.0.1: {} @@ -7393,7 +7495,7 @@ snapshots: decimal.js: 10.5.0 html-encoding-sniffer: 4.0.0 http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.6 + https-proxy-agent: 7.0.6(supports-color@10.0.0) is-potential-custom-element-name: 1.0.1 nwsapi: 2.2.20 parse5: 7.3.0 @@ -7501,7 +7603,7 @@ snapshots: dependencies: chalk: 5.4.1 commander: 13.1.0 - debug: 4.4.1 + debug: 4.4.1(supports-color@10.0.0) execa: 8.0.1 lilconfig: 3.1.3 listr2: 8.3.3 @@ -7610,6 +7712,10 @@ snapshots: dependencies: brace-expansion: 1.1.12 + minimatch@5.1.6: + dependencies: + brace-expansion: 2.0.2 + minimatch@9.0.5: dependencies: brace-expansion: 2.0.2 @@ -7670,6 +7776,16 @@ snapshots: is-docker: 2.2.1 is-wsl: 2.2.0 + openapi-typescript@7.8.0(typescript@5.8.3): + dependencies: + '@redocly/openapi-core': 1.34.3(supports-color@10.0.0) + ansi-colors: 4.1.3 + change-case: 5.4.4 + parse-json: 8.3.0 + supports-color: 10.0.0 + typescript: 5.8.3 + yargs-parser: 21.1.1 + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -7702,6 +7818,12 @@ snapshots: json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 + parse-json@8.3.0: + dependencies: + '@babel/code-frame': 7.27.1 + index-to-position: 1.1.0 + type-fest: 4.41.0 + parse5@7.3.0: dependencies: entities: 6.0.1 @@ -7753,6 +7875,8 @@ snapshots: optionalDependencies: fsevents: 2.3.2 + pluralize@8.0.0: {} + polished@4.3.1: dependencies: '@babel/runtime': 7.27.6 @@ -8153,6 +8277,8 @@ snapshots: dependencies: js-tokens: 9.0.1 + supports-color@10.0.0: {} + supports-color@7.2.0: dependencies: has-flag: 4.0.0 @@ -8291,6 +8417,8 @@ snapshots: type-fest@2.19.0: {} + type-fest@4.41.0: {} + typescript-eslint@8.35.1(eslint@9.30.0(jiti@2.4.2))(typescript@5.8.3): dependencies: '@typescript-eslint/eslint-plugin': 8.35.1(@typescript-eslint/parser@8.35.1(eslint@9.30.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.30.0(jiti@2.4.2))(typescript@5.8.3) @@ -8313,7 +8441,7 @@ snapshots: dependencies: '@antfu/install-pkg': 1.1.0 '@iconify/utils': 2.3.0 - debug: 4.4.1 + debug: 4.4.1(supports-color@10.0.0) local-pkg: 1.1.1 unplugin: 2.3.5 optionalDependencies: @@ -8338,6 +8466,8 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 + uri-js-replace@1.0.1: {} + uri-js@4.4.1: dependencies: punycode: 2.3.1 @@ -8359,7 +8489,7 @@ snapshots: vite-node@3.2.4(@types/node@22.15.34)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0): dependencies: cac: 6.7.14 - debug: 4.4.1 + 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.43.1)(tsx@4.20.3)(yaml@2.8.0) @@ -8405,7 +8535,7 @@ snapshots: '@vitest/spy': 3.2.4 '@vitest/utils': 3.2.4 chai: 5.2.0 - debug: 4.4.1 + debug: 4.4.1(supports-color@10.0.0) expect-type: 1.2.1 magic-string: 0.30.17 pathe: 2.0.3 @@ -8543,8 +8673,12 @@ snapshots: yallist@5.0.0: {} + yaml-ast-parser@0.0.43: {} + yaml@2.8.0: {} + yargs-parser@21.1.1: {} + yocto-queue@0.1.0: {} zod@3.25.67: {} diff --git a/apps/web/src/lib/openapi/schema.d.ts b/apps/web/src/lib/openapi/schema.d.ts new file mode 100644 index 00000000..c963d7c5 --- /dev/null +++ b/apps/web/src/lib/openapi/schema.d.ts @@ -0,0 +1,229 @@ +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + +export interface paths { + "/auth/callback": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * OAuth2 Auth Callback + * @description This route is used for OAuth authentication methods to verify and login/create an account. + */ + post: operations["post-v1-auth-callback"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/auth/me": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Current User + * @description Get the currently authenticated user's information. + */ + get: operations["get-auth-me"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; +} +export type webhooks = Record; +export interface components { + schemas: { + /** + * ErrorResponse + * @description This model is returned on server errors, it returns an error code (lookup code definitions in documentation), an error key, and a message. + */ + ErrorResponse: { + error: string; + message: string; + }; + /** + * UserContext + * @description This is the model used when returning from GetMe. Used often in middleware! + */ + UserContext: { + /** Format: uuid */ + userId: string; + name: string; + onboarded: boolean; + /** Format: uri */ + image?: string | null; + role: components["schemas"]["PlatformRole"]; + }; + /** + * PlatformRole + * @description A user's role on the platform. Either base permissions or elevated superuser perms. + * @enum {string} + */ + PlatformRole: "user" | "superuser"; + /** Session */ + Session: { + /** Format: uuid */ + id: string; + /** Format: uuid */ + user_id: string; + /** Format: date-time */ + expires_at: string; + ip_address?: string | null; + user_agent?: string | null; + }; + }; + responses: { + /** @description Unauthenticated: Requester is not currently authenticated. */ + Unauthenticated: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Unauthorized: User does not have access to this resource. */ + Unauthorized: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + "Bad-Request": { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; +} +export type $defs = Record; +export interface operations { + "post-v1-auth-callback": { + parameters: { + query: { + /** @description The OAuth code passed back from the provider. Part of the PKCE flow. */ + code: string; + /** @description The state containing a base64 encoded version of the nonce, provider, and redirect url. */ + state: string; + }; + header?: never; + path?: never; + cookie: { + /** @description The nonce for comparing against the callback state decoded to prevent CSRF attacks. */ + sh_auth_nonce: string; + }; + }; + requestBody?: never; + responses: { + /** @description OK: User is logged in successfully. For when the redirect field is empty. */ + 200: { + headers: { + /** @description Sets a sh_session cookie to signify auth status */ + "Set-Cookie"?: string; + [name: string]: unknown; + }; + content?: never; + }; + /** @description Found: Logged in and redirected to a requested location */ + 302: { + headers: { + /** @description Sets a sh_session cookie to signify auth status */ + "Set-Cookie"?: string; + [name: string]: unknown; + }; + content?: never; + }; + /** @description Bad Request: Something went wrong with the request queries or their properties */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Forbidden: Something went wrong verifying identity or authenticating. */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Bad Gateway: Authenticating OAuth server did not respond or user does not exist */ + 502: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + "get-auth-me": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: { + /** @description The authenticated session token/id */ + sh_session?: string; + }; + }; + requestBody?: never; + responses: { + /** @description OK: Current user data returned */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UserContext"]; + }; + }; + 401: components["responses"]["Unauthenticated"]; + /** @description Server Error: Something went terribly wrong on our end. */ + "5XX": { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; +} diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index 4d73ec11..23ae6460 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -1,10 +1,17 @@ { "files": [], "references": [ - { "path": "./tsconfig.app.json" }, - { "path": "./tsconfig.node.json" } + { + "path": "./tsconfig.app.json" + }, + { + "path": "./tsconfig.node.json" + } ], "compilerOptions": { - "types": ["vitest/globals", "@testing-library/jest-dom"] + "types": ["vitest/globals", "@testing-library/jest-dom"], + "module": "ESNext", + "moduleResolution": "Bundler", + "noUncheckedIndexedAccess": true } } diff --git a/shared/openapi/swagger.yaml b/shared/openapi/swagger.yaml index d294b1cb..b045b5a7 100644 --- a/shared/openapi/swagger.yaml +++ b/shared/openapi/swagger.yaml @@ -1,6 +1,4 @@ openapi: 3.0.0 -x-stoplight: - id: nutabra7kvljn info: title: SwampHacks Core version: '1.0' @@ -94,8 +92,6 @@ paths: error: oauth_gateway message: 'Failed to retrieve OAuth profile from {{provider}}' operationId: post-v1-auth-callback - x-stoplight: - id: ct784ewqj8k3i description: This route is used for OAuth authentication methods to verify and login/create an account. security: [] parameters: @@ -103,20 +99,22 @@ paths: type: string in: query name: code + required: true description: The OAuth code passed back from the provider. Part of the PKCE flow. - schema: type: string in: query name: state + required: true description: 'The state containing a base64 encoded version of the nonce, provider, and redirect url.' - schema: type: string in: cookie name: sh_auth_nonce + required: true description: The nonce for comparing against the callback state decoded to prevent CSRF attacks. tags: - Authentication - parameters: [] /auth/me: get: summary: Get Current User @@ -130,14 +128,14 @@ paths: examples: Normal user: value: - id: 497f6eca-6276-4993-bfeb-53cbbbba6f08 + userId: 497f6eca-6276-4993-bfeb-53cbbbba6f08 name: John Apple onboarded: false image: 'https://example.com/cat.jpeg' role: user Admin user: value: - id: 497f6eca-6276-4993-bfeb-53cbbbba6f08 + userId: 497f6eca-6276-4993-bfeb-53cbbbba6f08 name: Mark Scout onboarded: true image: 'http://lumoncorp.com/marks_pfp.jpeg' @@ -156,8 +154,6 @@ paths: error: internal_err message: Something went wrong on our end. operationId: get-auth-me - x-stoplight: - id: oruu4ylz0rtu8 description: Get the currently authenticated user's information. security: - sh_session: [] @@ -169,61 +165,45 @@ paths: description: The authenticated session token/id tags: - Authentication - parameters: [] components: schemas: ErrorResponse: title: ErrorResponse - x-stoplight: - id: dqn6jntknlmx1 type: object - x-examples: - Example 1: - code: A001 - error: invalid_op - message: That was an invalid operation. Please try something else. description: 'This model is returned on server errors, it returns an error code (lookup code definitions in documentation), an error key, and a message.' + required: + - error + - message properties: error: type: string - x-stoplight: - id: 2ukqzp5a2tmaf message: type: string - x-stoplight: - id: avnmp3mkbsdvj UserContext: title: UserContext - x-stoplight: - id: whmmi4ofk7mzg type: object description: This is the model used when returning from GetMe. Used often in middleware! + required: + - userId + - name + - onboarded + - role properties: userId: type: string - x-stoplight: - id: dutrkqcbamq9i format: uuid name: type: string - x-stoplight: - id: cxqkrp9hrxeot onboarded: type: boolean - x-stoplight: - id: jwsqc4yskzoax image: type: string - x-stoplight: - id: pahswvz0rf655 format: uri nullable: true role: $ref: '#/components/schemas/PlatformRole' PlatformRole: title: PlatformRole - x-stoplight: - id: 9700f7uzvtgay type: string enum: - user @@ -231,34 +211,26 @@ components: description: A user's role on the platform. Either base permissions or elevated superuser perms. Session: title: Session - x-stoplight: - id: 1ojcr8cwkebdk type: object + required: + - id + - user_id + - expires_at properties: id: type: string - x-stoplight: - id: vvmu11dsmwp2m format: uuid user_id: type: string - x-stoplight: - id: udxze31ht0b6g format: uuid expires_at: type: string - x-stoplight: - id: txciknpnhjrcu format: date-time ip_address: type: string - x-stoplight: - id: rg0dyly1hs7lm nullable: true user_agent: type: string - x-stoplight: - id: nwik1r68jxwyt nullable: true securitySchemes: sh_session_id: From 1b87a63aeae4547f67c015ba05ae4242d4ee80e7 Mon Sep 17 00:00:00 2001 From: Alexander Wang <98280966+AlexanderWangY@users.noreply.github.com> Date: Mon, 30 Jun 2025 23:55:13 -0400 Subject: [PATCH 09/52] feat: new pr template (#43) --- .github/pull_request_template.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 .github/pull_request_template.md diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..bcd6643d --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,31 @@ +## Description + + +## Linked Jira Ticket + + +## Type of Change + + +## Checklist +- [ ] My code follows the project's style guidelines +- [ ] I have commented on complex parts of the code +- [ ] I have updated documentation if necessary +- [ ] I have updated or added tests to cover my changes +- [ ] I have updated the OpenAPI YAML or other API schema files if applicable + +## Additional Notes + From ecefe7c8994a5bf11e9d9bdc38efd667c2949415 Mon Sep 17 00:00:00 2001 From: Alexander Wang <98280966+AlexanderWangY@users.noreply.github.com> Date: Mon, 30 Jun 2025 23:58:31 -0400 Subject: [PATCH 10/52] ref: change openapi yaml from swagger.yaml to core-api.yaml (#44) --- apps/web/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/package.json b/apps/web/package.json index a53ac09c..4a65ae3c 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -14,7 +14,7 @@ "format": "prettier --write ./src && git add --all", "storybook": "storybook dev -p 6006", "build-storybook": "storybook build", - "generate:openapi": "openapi-typescript ../../shared/openapi/swagger.yaml -o ./src/lib/openapi/schema.d.ts" + "generate:openapi": "openapi-typescript ../../shared/openapi/core-api.yaml -o ./src/lib/openapi/schema.d.ts" }, "lint-staged": { "*.{js,jsx,ts,tsx}": [ From 28961198b6f3091cf816f3206420f9fafdf914b4 Mon Sep 17 00:00:00 2001 From: Alexander Wang <98280966+AlexanderWangY@users.noreply.github.com> Date: Tue, 1 Jul 2025 00:37:29 -0400 Subject: [PATCH 11/52] release: add versioning and random yaml file?? (#45) --- apps/api/VERSION | 1 + apps/web/package.json | 2 +- shared/openapi/{swagger.yaml => core-api.yaml} | 0 3 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 apps/api/VERSION rename shared/openapi/{swagger.yaml => core-api.yaml} (100%) diff --git a/apps/api/VERSION b/apps/api/VERSION new file mode 100644 index 00000000..b82608c0 --- /dev/null +++ b/apps/api/VERSION @@ -0,0 +1 @@ +v0.1.0 diff --git a/apps/web/package.json b/apps/web/package.json index 4a65ae3c..f84c0e5c 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,7 +1,7 @@ { "name": "web", "private": true, - "version": "0.0.0", + "version": "0.1.0", "type": "module", "scripts": { "dev": "vite --host 0.0.0.0", diff --git a/shared/openapi/swagger.yaml b/shared/openapi/core-api.yaml similarity index 100% rename from shared/openapi/swagger.yaml rename to shared/openapi/core-api.yaml From 3da8e9bef6e8da5c71f9ec43dbd6e6034c1ab9b9 Mon Sep 17 00:00:00 2001 From: h1divp <71522316+h1divp@users.noreply.github.com> Date: Tue, 1 Jul 2025 22:52:08 -0400 Subject: [PATCH 12/52] docs: added more installation procedures --- apps/docs/src/docs/installation.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/apps/docs/src/docs/installation.md b/apps/docs/src/docs/installation.md index c70b511c..8504d8aa 100644 --- a/apps/docs/src/docs/installation.md +++ b/apps/docs/src/docs/installation.md @@ -1,5 +1,7 @@ # Gettings Started +## Normal setup + 1. Install [mkdocs](https://www.mkdocs.org/user-guide/installation/) 1. Install the python packages listed in core/apps/docs/requirements.txt 1. Run: @@ -11,6 +13,24 @@ mkdocs serve !!! warning If the site does not generate because there is still missing dependancies, you can find them using [mkdocs-get-deps](https://github.com/mkdocs/get-deps) +## For linux distributions which do not package python packages through pip + +This includes Arch Linux. + +1. Run +```bash +python -m venv env +source env/bin/activate +``` +1. Install packages +```bash +pip install -r requirements.txt +``` +1. Run with +```bash +mkdocs serve +``` + --- Now just make normal changes to the documentation and the site will update automatically. Make commits and push your changes when you're done! From 8b973972f569210f4d890782efd8e28d01b07c16 Mon Sep 17 00:00:00 2001 From: h1divp <71522316+h1divp@users.noreply.github.com> Date: Tue, 1 Jul 2025 22:53:55 -0400 Subject: [PATCH 13/52] feat: added swagger ui renderer plugin --- apps/docs/.gitignore | 210 +++++++++++++++++++++++++++++++++++++ apps/docs/mkdocs.yml | 1 + apps/docs/requirements.txt | 1 + 3 files changed, 212 insertions(+) create mode 100644 apps/docs/.gitignore diff --git a/apps/docs/.gitignore b/apps/docs/.gitignore new file mode 100644 index 00000000..abd8cb78 --- /dev/null +++ b/apps/docs/.gitignore @@ -0,0 +1,210 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[codz] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py.cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +#uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock +#poetry.toml + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. +# https://pdm-project.org/en/latest/usage/project/#working-with-version-control +#pdm.lock +#pdm.toml +.pdm-python +.pdm-build/ + +# pixi +# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. +#pixi.lock +# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one +# in the .venv directory. It is recommended not to include this directory in version control. +.pixi + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.envrc +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# Abstra +# Abstra is an AI-powered process automation framework. +# Ignore directories containing user credentials, local state, and settings. +# Learn more at https://abstra.io/docs +.abstra/ + +# Visual Studio Code +# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore +# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore +# and can be added to the global gitignore or merged into this file. However, if you prefer, +# you could uncomment the following to ignore the entire vscode folder +# .vscode/ + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc + +# Cursor +# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to +# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data +# refer to https://docs.cursor.com/context/ignore-files +.cursorignore +.cursorindexingignore + +# Marimo +marimo/_static/ +marimo/_lsp/ +__marimo__/ + +# Streamlit +.streamlit/secrets.toml diff --git a/apps/docs/mkdocs.yml b/apps/docs/mkdocs.yml index 3cb7d112..3df088a7 100644 --- a/apps/docs/mkdocs.yml +++ b/apps/docs/mkdocs.yml @@ -18,6 +18,7 @@ theme: - content.action.edit plugins: - search + - swagger-ui-tag copyright: > Copyright © 2025 SwampHacks diff --git a/apps/docs/requirements.txt b/apps/docs/requirements.txt index f79c7516..a94a8258 100644 --- a/apps/docs/requirements.txt +++ b/apps/docs/requirements.txt @@ -1,3 +1,4 @@ mkdocs mkdocs-material pymdown-extensions +mkdocs-swagger-ui-tag From 3c4417c73a7bf330d7213e696ac313e4e69d9de2 Mon Sep 17 00:00:00 2001 From: h1divp <71522316+h1divp@users.noreply.github.com> Date: Fri, 11 Jul 2025 18:44:05 -0400 Subject: [PATCH 14/52] improvement: improved even queries --- apps/api/internal/db/queries/events.sql | 92 +++++++------------------ 1 file changed, 23 insertions(+), 69 deletions(-) diff --git a/apps/api/internal/db/queries/events.sql b/apps/api/internal/db/queries/events.sql index 8c7dde7b..b8f72cf6 100644 --- a/apps/api/internal/db/queries/events.sql +++ b/apps/api/internal/db/queries/events.sql @@ -1,7 +1,7 @@ -- name: CreateEvent :one INSERT INTO events ( id, name, description, - location, loction_url, max_attendees, + location, location_url, max_attendees, application_open, application_close, rsvp_deadline, decision_release, start_time, end_time, website_url @@ -14,79 +14,33 @@ INSERT INTO events ( ) RETURNING *; --- name: GetEventByID: one +-- name: GetEventByID :one SELECT * FROM events WHERE id = $1; --- name: GetEventByLocation: many +-- name: GetEventByLocation :many SELECT * FROM events WHERE location = $1; --- name: UpdateEventName: exec -UPDATE events -SET name = $2 -WHERE id = $1; - --- name: UpdateEventDescription: exec -UPDATE events -SET description = $2 -WHERE id = $1; - --- name: UpdateEventLocation: exec -UPDATE events -SET location = $2 -WHERE id = $1; - --- name: UpdateEventLocationUrl: exec -UPDATE events -SET location_url = $2 -WHERE id = $1; - --- name: UpdateEventMaxAttendees: exec -UPDATE events -SET max_attendees = $2 -WHERE id = $1; - --- name: UpdateEventApplicationOpen: exec -UPDATE events -SET application_open = $2 -WHERE id = $1; - --- name: UpdateEventApplicationClose: exec -UPDATE events -SET application_close = $2 -WHERE id = $1; - --- name: UpdateEventRsvpDeadline: exec -UPDATE events -SET rsvp_deadline = $2 -WHERE id = $1; - --- name: UpdateEventDecisionRelease: exec -UPDATE events -SET decision_release = $2 -WHERE id = $1; - --- name: UpdateEventStartTime: exec -UPDATE events -SET start_time = $2 -WHERE id = $1; - --- name: UpdateEventEndTime: exec -UPDATE events -SET end_time = $2 -WHERE id = $1; - --- name: UpdateEventIsPublished: exec -UPDATE events -SET is_published = $2 -WHERE id = $1; - --- name: UpdateEventWebsiteUrl: exec -UPDATE events -SET website_url = $2 -WHERE id = $1; - --- name: DeleteEvent: exec +-- name: UpdateEvent :exec +UPDATE events +SET + name = CASE WHEN @name_do_update::boolean THEN @name ELSE name END, + description = CASE WHEN @description_do_update::boolean THEN @description ELSE description END, + location = CASE WHEN @location_do_update::boolean THEN @location ELSE location END, + location_url = CASE WHEN @location_url_do_update::boolean THEN @location_url ELSE location_url END, + max_attendees = CASE WHEN @max_attendees_do_update::boolean THEN @max_attendees ELSE max_attendees END, + application_open = CASE WHEN @application_open_do_update::boolean THEN @application_open ELSE application_open END, + application_close = CASE WHEN @application_close_do_update::boolean THEN @application_close ELSE application_close END, + rsvp_deadline = CASE WHEN @rsvp_deadline_do_update::boolean THEN @rsvp_deadline ELSE rsvp_deadline END, + decision_release = CASE WHEN @decision_release_do_update::boolean THEN @decision_release ELSE decision_release END, + 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, + website_url = CASE WHEN @website_url_do_update::boolean THEN @website_url ELSE website_url END, + is_published = CASE WHEN @is_published_do_update::boolean THEN @is_published ELSE is_published END +WHERE + id = @id::uuid; + +-- name: DeleteEvent :exec DELETE FROM events WHERE id = $1; From e9928d484fe8585cc3ba3b2949b61a9da536c881 Mon Sep 17 00:00:00 2001 From: h1divp <71522316+h1divp@users.noreply.github.com> Date: Fri, 11 Jul 2025 18:44:32 -0400 Subject: [PATCH 15/52] chore: sqlc generation --- apps/api/internal/db/sqlc/events.sql.go | 248 ++++++++++++++++++++++++ apps/api/internal/db/sqlc/querier.go | 5 + 2 files changed, 253 insertions(+) create mode 100644 apps/api/internal/db/sqlc/events.sql.go diff --git a/apps/api/internal/db/sqlc/events.sql.go b/apps/api/internal/db/sqlc/events.sql.go new file mode 100644 index 00000000..e10d6cd9 --- /dev/null +++ b/apps/api/internal/db/sqlc/events.sql.go @@ -0,0 +1,248 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 +// source: events.sql + +package sqlc + +import ( + "context" + "time" + + "github.com/google/uuid" +) + +const createEvent = `-- name: CreateEvent :one +INSERT INTO events ( + id, name, description, + location, location_url, max_attendees, + application_open, application_close, rsvp_deadline, decision_release, + start_time, end_time, + website_url +) VALUES ( + $1, $2, $3, + $4, $5, $6, + $7, $8, $9, $10, + $11, $12, + $13 +) +RETURNING id, name, description, location, location_url, max_attendees, application_open, application_close, rsvp_deadline, decision_release, start_time, end_time, website_url, is_published, created_at, updated_at +` + +type CreateEventParams struct { + ID uuid.UUID `json:"id"` + Name string `json:"name"` + Description *string `json:"description"` + Location *string `json:"location"` + LocationUrl *string `json:"location_url"` + MaxAttendees *int32 `json:"max_attendees"` + ApplicationOpen time.Time `json:"application_open"` + ApplicationClose time.Time `json:"application_close"` + RsvpDeadline *time.Time `json:"rsvp_deadline"` + DecisionRelease *time.Time `json:"decision_release"` + StartTime time.Time `json:"start_time"` + EndTime time.Time `json:"end_time"` + WebsiteUrl *string `json:"website_url"` +} + +func (q *Queries) CreateEvent(ctx context.Context, arg CreateEventParams) (Event, error) { + row := q.db.QueryRow(ctx, createEvent, + arg.ID, + arg.Name, + arg.Description, + arg.Location, + arg.LocationUrl, + arg.MaxAttendees, + arg.ApplicationOpen, + arg.ApplicationClose, + arg.RsvpDeadline, + arg.DecisionRelease, + arg.StartTime, + arg.EndTime, + arg.WebsiteUrl, + ) + var i Event + err := row.Scan( + &i.ID, + &i.Name, + &i.Description, + &i.Location, + &i.LocationUrl, + &i.MaxAttendees, + &i.ApplicationOpen, + &i.ApplicationClose, + &i.RsvpDeadline, + &i.DecisionRelease, + &i.StartTime, + &i.EndTime, + &i.WebsiteUrl, + &i.IsPublished, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const deleteEvent = `-- name: DeleteEvent :exec +DELETE FROM events +WHERE id = $1 +` + +func (q *Queries) DeleteEvent(ctx context.Context, id uuid.UUID) error { + _, err := q.db.Exec(ctx, deleteEvent, id) + return err +} + +const getEventByID = `-- name: GetEventByID :one +SELECT id, name, description, location, location_url, max_attendees, application_open, application_close, rsvp_deadline, decision_release, start_time, end_time, website_url, is_published, created_at, updated_at FROM events +WHERE id = $1 +` + +func (q *Queries) GetEventByID(ctx context.Context, id uuid.UUID) (Event, error) { + row := q.db.QueryRow(ctx, getEventByID, id) + var i Event + err := row.Scan( + &i.ID, + &i.Name, + &i.Description, + &i.Location, + &i.LocationUrl, + &i.MaxAttendees, + &i.ApplicationOpen, + &i.ApplicationClose, + &i.RsvpDeadline, + &i.DecisionRelease, + &i.StartTime, + &i.EndTime, + &i.WebsiteUrl, + &i.IsPublished, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const getEventByLocation = `-- name: GetEventByLocation :many +SELECT id, name, description, location, location_url, max_attendees, application_open, application_close, rsvp_deadline, decision_release, start_time, end_time, website_url, is_published, created_at, updated_at FROM events +WHERE location = $1 +` + +func (q *Queries) GetEventByLocation(ctx context.Context, location *string) ([]Event, error) { + rows, err := q.db.Query(ctx, getEventByLocation, location) + if err != nil { + return nil, err + } + defer rows.Close() + items := []Event{} + for rows.Next() { + var i Event + if err := rows.Scan( + &i.ID, + &i.Name, + &i.Description, + &i.Location, + &i.LocationUrl, + &i.MaxAttendees, + &i.ApplicationOpen, + &i.ApplicationClose, + &i.RsvpDeadline, + &i.DecisionRelease, + &i.StartTime, + &i.EndTime, + &i.WebsiteUrl, + &i.IsPublished, + &i.CreatedAt, + &i.UpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const updateEvent = `-- name: UpdateEvent :exec +UPDATE events +SET + name = CASE WHEN $1::boolean THEN $2 ELSE name END, + description = CASE WHEN $3::boolean THEN $4 ELSE description END, + location = CASE WHEN $5::boolean THEN $6 ELSE location END, + location_url = CASE WHEN $7::boolean THEN $8 ELSE location_url END, + max_attendees = CASE WHEN $9::boolean THEN $10 ELSE max_attendees END, + application_open = CASE WHEN $11::boolean THEN $12 ELSE application_open END, + application_close = CASE WHEN $13::boolean THEN $14 ELSE application_close END, + rsvp_deadline = CASE WHEN $15::boolean THEN $16 ELSE rsvp_deadline END, + decision_release = CASE WHEN $17::boolean THEN $18 ELSE decision_release END, + start_time = CASE WHEN $19::boolean THEN $20 ELSE start_time END, + end_time = CASE WHEN $21::boolean THEN $22 ELSE end_time END, + website_url = CASE WHEN $23::boolean THEN $24 ELSE website_url END, + is_published = CASE WHEN $25::boolean THEN $26 ELSE is_published END +WHERE + id = $27::uuid +` + +type UpdateEventParams struct { + NameDoUpdate bool `json:"name_do_update"` + Name string `json:"name"` + DescriptionDoUpdate bool `json:"description_do_update"` + Description *string `json:"description"` + LocationDoUpdate bool `json:"location_do_update"` + Location *string `json:"location"` + LocationUrlDoUpdate bool `json:"location_url_do_update"` + LocationUrl *string `json:"location_url"` + MaxAttendeesDoUpdate bool `json:"max_attendees_do_update"` + MaxAttendees *int32 `json:"max_attendees"` + ApplicationOpenDoUpdate bool `json:"application_open_do_update"` + ApplicationOpen time.Time `json:"application_open"` + ApplicationCloseDoUpdate bool `json:"application_close_do_update"` + ApplicationClose time.Time `json:"application_close"` + RsvpDeadlineDoUpdate bool `json:"rsvp_deadline_do_update"` + RsvpDeadline *time.Time `json:"rsvp_deadline"` + DecisionReleaseDoUpdate bool `json:"decision_release_do_update"` + DecisionRelease *time.Time `json:"decision_release"` + StartTimeDoUpdate bool `json:"start_time_do_update"` + StartTime time.Time `json:"start_time"` + EndTimeDoUpdate bool `json:"end_time_do_update"` + EndTime time.Time `json:"end_time"` + WebsiteUrlDoUpdate bool `json:"website_url_do_update"` + WebsiteUrl *string `json:"website_url"` + IsPublishedDoUpdate bool `json:"is_published_do_update"` + IsPublished *bool `json:"is_published"` + ID uuid.UUID `json:"id"` +} + +func (q *Queries) UpdateEvent(ctx context.Context, arg UpdateEventParams) error { + _, err := q.db.Exec(ctx, updateEvent, + arg.NameDoUpdate, + arg.Name, + arg.DescriptionDoUpdate, + arg.Description, + arg.LocationDoUpdate, + arg.Location, + arg.LocationUrlDoUpdate, + arg.LocationUrl, + arg.MaxAttendeesDoUpdate, + arg.MaxAttendees, + arg.ApplicationOpenDoUpdate, + arg.ApplicationOpen, + arg.ApplicationCloseDoUpdate, + arg.ApplicationClose, + arg.RsvpDeadlineDoUpdate, + arg.RsvpDeadline, + arg.DecisionReleaseDoUpdate, + arg.DecisionRelease, + arg.StartTimeDoUpdate, + arg.StartTime, + arg.EndTimeDoUpdate, + arg.EndTime, + arg.WebsiteUrlDoUpdate, + arg.WebsiteUrl, + arg.IsPublishedDoUpdate, + arg.IsPublished, + arg.ID, + ) + return err +} diff --git a/apps/api/internal/db/sqlc/querier.go b/apps/api/internal/db/sqlc/querier.go index 324eb406..d3266879 100644 --- a/apps/api/internal/db/sqlc/querier.go +++ b/apps/api/internal/db/sqlc/querier.go @@ -12,20 +12,25 @@ import ( type Querier interface { CreateAccount(ctx context.Context, arg CreateAccountParams) (AuthAccount, error) + CreateEvent(ctx context.Context, arg CreateEventParams) (Event, error) CreateSession(ctx context.Context, arg CreateSessionParams) (AuthSession, error) CreateUser(ctx context.Context, arg CreateUserParams) (AuthUser, error) DeleteAccount(ctx context.Context, arg DeleteAccountParams) error + DeleteEvent(ctx context.Context, id uuid.UUID) error DeleteExpiredSession(ctx context.Context) error DeleteUser(ctx context.Context, id uuid.UUID) error GetActiveSessionUserInfo(ctx context.Context, id uuid.UUID) (GetActiveSessionUserInfoRow, error) GetByProviderAndAccountID(ctx context.Context, arg GetByProviderAndAccountIDParams) (AuthAccount, error) GetByUserID(ctx context.Context, userID uuid.UUID) ([]AuthAccount, error) + GetEventByID(ctx context.Context, id uuid.UUID) (Event, error) + GetEventByLocation(ctx context.Context, location *string) ([]Event, error) GetSessionByID(ctx context.Context, id uuid.UUID) (AuthSession, error) GetSessionsByUserID(ctx context.Context, userID uuid.UUID) ([]AuthSession, error) GetUserByEmail(ctx context.Context, email *string) (AuthUser, error) GetUserByID(ctx context.Context, id uuid.UUID) (AuthUser, error) InvalidateSessionByID(ctx context.Context, id uuid.UUID) error TouchSession(ctx context.Context, arg TouchSessionParams) error + UpdateEvent(ctx context.Context, arg UpdateEventParams) error UpdateSessionExpiration(ctx context.Context, arg UpdateSessionExpirationParams) error UpdateTokens(ctx context.Context, arg UpdateTokensParams) error UpdateUser(ctx context.Context, arg UpdateUserParams) error From 65f050b22d3f6d6b4e45ae4cdbfa5ae9191ce17b Mon Sep 17 00:00:00 2001 From: Phoenix <71522316+h1divp@users.noreply.github.com> Date: Tue, 1 Jul 2025 22:10:15 -0400 Subject: [PATCH 16/52] improvement: update template jira link --- .github/pull_request_template.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index bcd6643d..96d380ca 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -5,7 +5,7 @@ Summarize changes, try to be specific. ## Linked Jira Ticket ## Type of Change From 2b5ca8ffb2472c7d4f32745df9c4593dece7446d Mon Sep 17 00:00:00 2001 From: Phoenix <71522316+h1divp@users.noreply.github.com> Date: Tue, 1 Jul 2025 22:11:00 -0400 Subject: [PATCH 17/52] Add hyphen --- .github/pull_request_template.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 96d380ca..e01a1dc1 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -5,7 +5,7 @@ Summarize changes, try to be specific. ## Linked Jira Ticket ## Type of Change From 8070e4691068075d22a4e6d9ad267d2813ed6d79 Mon Sep 17 00:00:00 2001 From: h1divp <71522316+h1divp@users.noreply.github.com> Date: Wed, 2 Jul 2025 14:16:42 -0400 Subject: [PATCH 18/52] refactor: changed referance to link to other site that renders api docs --- apps/docs/mkdocs.yml | 3 +-- apps/docs/requirements.txt | 1 - apps/docs/src/api/reference.md | 1 - 3 files changed, 1 insertion(+), 4 deletions(-) delete mode 100644 apps/docs/src/api/reference.md diff --git a/apps/docs/mkdocs.yml b/apps/docs/mkdocs.yml index 3df088a7..21a8abd8 100644 --- a/apps/docs/mkdocs.yml +++ b/apps/docs/mkdocs.yml @@ -18,7 +18,6 @@ theme: - content.action.edit plugins: - search - - swagger-ui-tag copyright: > Copyright © 2025 SwampHacks @@ -50,7 +49,7 @@ nav: - API: - Overview: api/index.md - Installation: api/installation.md - - Reference: api/reference.md + - OpenAPI: 'https://core.apidocumentation.com/guide/swamphacks-core-api' - Discord Bot: - Overview: discord-bot/index.md - Installation: discord-bot/installation.md diff --git a/apps/docs/requirements.txt b/apps/docs/requirements.txt index a94a8258..f79c7516 100644 --- a/apps/docs/requirements.txt +++ b/apps/docs/requirements.txt @@ -1,4 +1,3 @@ mkdocs mkdocs-material pymdown-extensions -mkdocs-swagger-ui-tag diff --git a/apps/docs/src/api/reference.md b/apps/docs/src/api/reference.md deleted file mode 100644 index 9e2397bf..00000000 --- a/apps/docs/src/api/reference.md +++ /dev/null @@ -1 +0,0 @@ -TODO: Render /shared/openapi/swagger.yaml From 4375eee26d17109f56a2f928369a7fa0bc322178 Mon Sep 17 00:00:00 2001 From: Hieu Nguyen <76720778+hieunguyent12@users.noreply.github.com> Date: Sun, 6 Jul 2025 22:36:16 -0500 Subject: [PATCH 19/52] feat: added event card button component (#49) * feat: added event card button component * feat: added hover state to buttons * fix: added className prop to event button * fix: muted fast refresh eslint * style: removed camelcase in css files * ref: change from loop to filter and map for storybook options --------- Co-authored-by: Alexander Wang --- apps/web/src/components/ui/Button/Button.tsx | 44 +++ apps/web/src/components/ui/Button/index.tsx | 48 +--- .../src/features/Event/applicationStatus.ts | 61 +++- .../features/Event/components/EventButton.tsx | 43 +++ .../components/stories/EventBadge.stories.tsx | 35 +-- .../stories/EventButton.stories.tsx | 92 ++++++ apps/web/src/theme.css | 265 ++++++++++++++++-- 7 files changed, 503 insertions(+), 85 deletions(-) create mode 100644 apps/web/src/components/ui/Button/Button.tsx create mode 100644 apps/web/src/features/Event/components/EventButton.tsx create mode 100644 apps/web/src/features/Event/components/stories/EventButton.stories.tsx diff --git a/apps/web/src/components/ui/Button/Button.tsx b/apps/web/src/components/ui/Button/Button.tsx new file mode 100644 index 00000000..f6f2ca37 --- /dev/null +++ b/apps/web/src/components/ui/Button/Button.tsx @@ -0,0 +1,44 @@ +// TODO: Remove the eslint-disable line and fix the fast refresh issue +import { forwardRef } from "react"; +// RAC = React Aria Components +import { + Button as RAC_Button, + type ButtonProps as RAC_ButtonProps, +} from "react-aria-components"; +import { tv, type VariantProps } from "tailwind-variants"; + +// Define styles with tailwind-variants +export const button = tv({ + base: "inline-flex cursor-pointer px-4 py-2 items-center justify-center rounded-md text-sm font-medium focus:outline-none focus:ring-2 focus:ring-offset-2", + variants: { + color: { + primary: + "bg-button-primary text-white hover:bg-button-primary-hover focus:ring-blue-500", + secondary: "bg-gray-600 text-white hover:bg-gray-700 focus:ring-gray-500", + danger: "bg-red-600 text-white hover:bg-red-700 focus:ring-red-500", + }, + }, + defaultVariants: { + color: "primary", + }, +}); + +// Combine Tailwind variant types with RAC props +type ButtonVariants = VariantProps; + +export interface ButtonProps extends ButtonVariants, RAC_ButtonProps { + className?: string; +} + +const Button = forwardRef( + ({ color, className, ...props }, ref) => { + // Combine the props to apply styles from tailwind-variants along with React Aria button props + const btnClassName = button({ color, className }); + + return ; + }, +); + +Button.displayName = "Button"; + +export { Button }; diff --git a/apps/web/src/components/ui/Button/index.tsx b/apps/web/src/components/ui/Button/index.tsx index 9fd20e33..e22c29ad 100644 --- a/apps/web/src/components/ui/Button/index.tsx +++ b/apps/web/src/components/ui/Button/index.tsx @@ -1,47 +1 @@ -import { forwardRef } from "react"; -// RAC = React Aria Components -import { - Button as RAC_Button, - type ButtonProps as RAC_ButtonProps, -} from "react-aria-components"; -import { tv, type VariantProps } from "tailwind-variants"; - -// Define styles with tailwind-variants -const button = tv({ - base: "inline-flex cursor-pointer px-4 py-2 items-center justify-center rounded-md text-sm font-medium focus:outline-none focus:ring-2 focus:ring-offset-2", - variants: { - color: { - primary: - "bg-button-primary text-white hover:bg-button-primary-hover focus:ring-blue-500", - secondary: "bg-gray-600 text-white hover:bg-gray-700 focus:ring-gray-500", - danger: "bg-red-600 text-white hover:bg-red-700 focus:ring-red-500", - }, - }, - defaultVariants: { - color: "primary", - }, -}); - -// Combine Tailwind variant types with RAC props -type ButtonVariants = VariantProps; - -interface ButtonProps extends ButtonVariants, RAC_ButtonProps {} - -const Button = forwardRef( - ({ color, className, ...props }, ref) => { - // Combine the props to apply styles from tailwind-variants along with React Aria button props - const btnClassName = button({ color }); - - return ( - - ); - }, -); - -Button.displayName = "Button"; - -export { Button }; +export * from "./Button"; diff --git a/apps/web/src/features/Event/applicationStatus.ts b/apps/web/src/features/Event/applicationStatus.ts index e505bbac..1c41f2e2 100644 --- a/apps/web/src/features/Event/applicationStatus.ts +++ b/apps/web/src/features/Event/applicationStatus.ts @@ -14,6 +14,11 @@ type ApplicationStatus = { className: string; text: string; icon?: ComponentType>; + + button: { + className: string; + text: string; + }; }; }; @@ -26,51 +31,101 @@ const applicationStatus = defineStatus({ className: "bg-badge-bg-rejected text-badge-text-rejected", text: "Rejected", icon: TablerBan, + button: { + className: + "bg-event-button-bg-rejected text-event-button-text-rejected hover:bg-event-button-bg-rejected-hover", + text: "Learn more", + }, }, attending: { className: "bg-badge-bg-attending text-badge-text-attending", text: "Attending", icon: TablerUserCheck, + button: { + className: + "bg-event-button-bg-attending text-event-button-text-attending hover:bg-event-button-bg-attending-hover", + text: "Dashboard", + }, }, accepted: { className: "bg-badge-bg-accepted text-badge-text-accepted", text: "Accepted", icon: TablerConfetti, + button: { + className: + "bg-event-button-bg-accepted-attending text-event-button-text-accepted-attending hover:bg-event-button-bg-accepted-attending-hover", + text: "I'm Attending", + }, }, waitlisted: { className: "bg-badge-bg-waitlisted text-badge-text-waitlisted", text: "Waitlisted", icon: TablerClockPause, + button: { + className: + "bg-event-button-bg-waitlisted text-event-button-text-waitlisted hover:bg-event-button-bg-waitlisted-hover", + text: "What's Next?", + }, }, underReview: { - className: "bg-badge-bg-underReview text-badge-text-underReview", + className: "bg-badge-bg-under-review text-badge-text-under-review", text: "Under Review", icon: TablerHourglassFilled, + button: { + className: + "bg-event-button-bg-under-review text-event-button-text-under-review hover:bg-event-button-bg-under-review-hover", + text: "Dashboard", + }, }, notApplied: { - className: "bg-badge-bg-notApplied text-badge-text-notApplied", + className: "bg-badge-bg-not-applied text-badge-text-not-applied", text: "Not Applied", icon: TablerPointFilled, + button: { + className: + "bg-event-button-bg-not-applied text-event-button-text-not-applied hover:bg-event-button-bg-not-applied-hover", + text: "Apply Now", + }, }, staff: { className: "bg-badge-bg-staff text-badge-text-staff", text: "Staff", icon: TablerId, + button: { + className: + "bg-event-button-bg-staff text-event-button-text-staff hover:bg-event-button-bg-staff-hover", + text: "Dashboard", + }, }, admin: { className: "bg-badge-bg-admin text-badge-text-admin", text: "Admin", icon: TablerSettings2, + button: { + className: + "bg-event-button-bg-admin text-event-button-text-admin hover:bg-event-button-bg-admin-hover", + text: "Dashboard", + }, }, notGoing: { - className: "bg-badge-bg-notGoing text-badge-text-notGoing", + className: "bg-badge-bg-not-going text-badge-text-not-going", text: "Not Going", icon: TablerBan, + button: { + className: + "bg-event-button-bg-not-going text-event-button-text-not-going hover:bg-event-button-bg-not-going-hover", + text: "Help Us Improve", + }, }, completed: { className: "bg-badge-bg-completed text-badge-text-completed", text: "Completed", icon: TablerCalendarCheck, + button: { + className: + "bg-event-button-bg-completed text-event-button-text-completed hover:bg-event-button-bg-completed-hover", + text: "Event Summary", + }, }, }); diff --git a/apps/web/src/features/Event/components/EventButton.tsx b/apps/web/src/features/Event/components/EventButton.tsx new file mode 100644 index 00000000..7d7166f9 --- /dev/null +++ b/apps/web/src/features/Event/components/EventButton.tsx @@ -0,0 +1,43 @@ +// TODO: Remove the eslint-disable line and fix the fast refresh issue +import { tv } from "tailwind-variants"; +import applicationStatus from "../applicationStatus"; +import { Button, button, type ButtonProps } from "@/components/ui/Button"; +import { cn } from "@/utils/cn"; + +type ApplicationStatusTypes = keyof typeof applicationStatus; + +const applicationStatusVariants = Object.fromEntries( + Object.entries(applicationStatus).map(([key, value]) => [ + key, + value.button.className, + ]), +) as { + [K in ApplicationStatusTypes]: (typeof applicationStatus)[K]["button"]["className"]; +}; + +export const eventButton = tv({ + extend: button, + variants: { + color: {}, + status: applicationStatusVariants, + }, +}); + +interface EventButtonProps extends ButtonProps { + status: ApplicationStatusTypes; +} + +const EventButton = ({ status: statusProp, className }: EventButtonProps) => { + const eventButtonClassName = eventButton({ + status: statusProp, + className, + }); + + return ( + + ); +}; + +export { EventButton }; diff --git a/apps/web/src/features/Event/components/stories/EventBadge.stories.tsx b/apps/web/src/features/Event/components/stories/EventBadge.stories.tsx index afe3858b..37633e45 100644 --- a/apps/web/src/features/Event/components/stories/EventBadge.stories.tsx +++ b/apps/web/src/features/Event/components/stories/EventBadge.stories.tsx @@ -1,24 +1,25 @@ import type { Meta, StoryObj } from "@storybook/react"; import { EventBadge, eventBadge } from "../EventBadge"; -const entries = []; - -for (let i = 0; i < eventBadge.variantKeys.length; i++) { - const key = eventBadge.variantKeys[i]; - - if (key === "type") continue; - - entries.push([ - key, - { - control: { - type: "select", +/** + * Generates the Storybook `argTypes` configuration dynamically based on + * the available variants in `eventBadge`. + * + * Filter out the `type` variant since it is not relevant for the storybook options. + */ +const argTypes = Object.fromEntries( + eventBadge.variantKeys + .filter((key) => key !== "type") + .map((key) => [ + key, + { + control: { + type: "select", + }, + options: Object.keys(eventBadge.variants[key]), }, - options: Object.keys(eventBadge.variants[key]), - }, - ]); -} -const argTypes = Object.fromEntries(entries); + ]), +); const meta = { component: EventBadge, diff --git a/apps/web/src/features/Event/components/stories/EventButton.stories.tsx b/apps/web/src/features/Event/components/stories/EventButton.stories.tsx new file mode 100644 index 00000000..828b3e3d --- /dev/null +++ b/apps/web/src/features/Event/components/stories/EventButton.stories.tsx @@ -0,0 +1,92 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { EventButton, eventButton } from "../EventButton"; + +/** + * Generates the Storybook `argTypes` configuration dynamically based on + * the available variants in `eventButton`. + * + * Filter out the `color` variant since it is not relevant for the storybook options. + */ +const argTypes = Object.fromEntries( + eventButton.variantKeys + .filter((key) => key !== "color") + .map((key) => [ + key, + { + control: { + type: "select", + }, + options: Object.keys(eventButton.variants[key]), + }, + ]), +); + +const meta = { + component: EventButton, + title: "UI/Event Button", + tags: ["autodocs"], + argTypes, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Rejected: Story = { + args: { + status: "rejected", + }, +}; + +export const Attending: Story = { + args: { + status: "attending", + }, +}; + +export const Accepted: Story = { + args: { + status: "accepted", + }, +}; + +export const Waitlisted: Story = { + args: { + status: "waitlisted", + }, +}; + +export const UnderReview: Story = { + args: { + status: "underReview", + }, +}; + +export const NotApplied: Story = { + args: { + status: "notApplied", + }, +}; + +export const Staff: Story = { + args: { + status: "staff", + }, +}; + +export const Admin: Story = { + args: { + status: "admin", + }, +}; + +export const NotGoing: Story = { + args: { + status: "notGoing", + }, +}; + +export const Completed: Story = { + args: { + status: "completed", + }, +}; diff --git a/apps/web/src/theme.css b/apps/web/src/theme.css index 4beddc9a..c42452d2 100644 --- a/apps/web/src/theme.css +++ b/apps/web/src/theme.css @@ -16,16 +16,17 @@ --button-outline-hover: oklch(0.5465 0.2455 262.87 / 25%); --button-outline-disabled: oklch(0.8071 0.10065 250.4462 / 25%); + /* BADGES */ --badge-bg-default: var(--color-neutral-200); --badge-bg-rejected: var(--color-red-200); --badge-bg-attending: var(--color-blue-200); --badge-bg-accepted: var(--color-green-200); --badge-bg-waitlisted: var(--color-violet-200); - --badge-bg-underReview: var(--color-orange-200); - --badge-bg-notApplied: var(--color-neutral-300); + --badge-bg-under-review: var(--color-orange-200); + --badge-bg-not-applied: var(--color-neutral-300); --badge-bg-staff: var(--color-sky-200); --badge-bg-admin: var(--color-fuchsia-200); - --badge-bg-notGoing: var(--color-red-200); + --badge-bg-not-going: var(--color-red-200); --badge-bg-completed: var(--color-indigo-200); --badge-text-default: var(--color-neutral-600); @@ -33,12 +34,80 @@ --badge-text-rejected: var(--color-red-600); --badge-text-accepted: var(--color-green-600); --badge-text-waitlisted: var(--color-violet-600); - --badge-text-underReview: var(--color-orange-600); - --badge-text-notApplied: var(--color-stone-600); + --badge-text-under-review: var(--color-orange-600); + --badge-text-not-applied: var(--color-stone-600); --badge-text-staff: var(--color-sky-600); --badge-text-admin: var(--color-fuchsia-600); - --badge-text-notGoing: var(--color-red-600); + --badge-text-not-going: var(--color-red-600); --badge-text-completed: var(--color-indigo-600); + + /* EVENT CARD BUTTONS */ + --event-button-bg-not-applied: var(--color-cyan-700); + --event-button-bg-not-applied-hover: var(--color-cyan-800); + --event-button-bg-under-review: color-mix( + in oklab, + var(--color-cyan-700) 25%, + transparent + ); + --event-button-bg-under-review-hover: color-mix( + in oklab, + var(--color-cyan-700) 15%, + transparent + ); + --event-button-bg-accepted-attending: var(--color-green-600); + --event-button-bg-accepted-attending-hover: var(--color-green-700); + --event-button-bg-accepted-not-going: var(--color-neutral-200); + --event-button-bg-accepted-not-going-hover: var(--color-neutral-300); + --event-button-bg-rejected: var(--color-neutral-200); + --event-button-bg-rejected-hover: var(--color-neutral-300); + --event-button-bg-attending: color-mix( + in oklab, + var(--color-cyan-700) 25%, + transparent + ); + --event-button-bg-attending-hover: color-mix( + in oklab, + var(--color-cyan-700) 30%, + transparent + ); + --event-button-bg-waitlisted: var(--color-violet-200); + --event-button-bg-waitlisted-hover: var(--color-violet-300); + --event-button-bg-staff: color-mix( + in oklab, + var(--color-cyan-700) 25%, + transparent + ); + --event-button-bg-staff-hover: color-mix( + in oklab, + var(--color-cyan-700) 30%, + transparent + ); + --event-button-bg-admin: color-mix( + in oklab, + var(--color-cyan-700) 25%, + transparent + ); + --event-button-bg-admin-hover: color-mix( + in oklab, + var(--color-cyan-700) 30%, + transparent + ); + --event-button-bg-not-going: var(--color-neutral-200); + --event-button-bg-not-going-hover: var(--color-neutral-300); + --event-button-bg-completed: var(--color-neutral-200); + --event-button-bg-completed-hover: var(--color-neutral-300); + + --event-button-text-not-applied: var(--color-neutral-50); + --event-button-text-under-review: var(--color-cyan-900); + --event-button-text-accepted-attending: var(--color-neutral-50); + --event-button-text-accepted-not-going: var(--color-zinc-900); + --event-button-text-rejected: var(--color-zinc-900); + --event-button-text-attending: var(--color-cyan-900); + --event-button-text-waitlisted: var(--color-violet-600); + --event-button-text-staff: var(--color-cyan-900); + --event-button-text-admin: var(--color-cyan-900); + --event-button-text-not-going: var(--color-zinc-900); + --event-button-text-completed: var(--color-zinc-900); } .dark { @@ -59,16 +128,17 @@ --button-outline-hover: oklch(0.5465 0.2455 262.87 / 25%); --button-outline-disabled: oklch(0.8071 0.10065 250.4462 / 25%); + /* BADGES */ --badge-bg-default: var(--color-neutral-200); --badge-bg-rejected: oklch(0.3378 0.0595 20.68); --badge-bg-attending: oklch(0.3078 0.0681 265.53); --badge-bg-accepted: oklch(0.3701 0.0671 153.92); --badge-bg-waitlisted: oklch(0.3838 0.082 292.43); - --badge-bg-underReview: oklch(0.3668 0.0531 65.94); - --badge-bg-notApplied: oklch(0.3715 0 0); + --badge-bg-under-review: oklch(0.3668 0.0531 65.94); + --badge-bg-not-applied: oklch(0.3715 0 0); --badge-bg-staff: oklch(0.3339 0.0442 235.27); --badge-bg-admin: oklch(0.3526 0.0681 318.27); - --badge-bg-notGoing: oklch(0.3378 0.0595 20.68); + --badge-bg-not-going: oklch(0.3378 0.0595 20.68); --badge-bg-completed: var(--color-slate-700); --badge-text-default: var(--color-neutral-600); @@ -76,12 +146,112 @@ --badge-text-rejected: var(--color-red-400); --badge-text-accepted: var(--color-green-300); --badge-text-waitlisted: oklch(0.9244 0.041 296.28); - --badge-text-underReview: var(--color-orange-300); - --badge-text-notApplied: var(--color-stone-300); + --badge-text-under-review: var(--color-orange-300); + --badge-text-not-applied: var(--color-stone-300); --badge-text-staff: var(--color-sky-200); --badge-text-admin: var(--color-fuchsia-400); - --badge-text-notGoing: var(--color-red-400); + --badge-text-not-going: var(--color-red-400); --badge-text-completed: var(--color-slate-300); + + /* EVENT CARD BUTTONS */ + --event-button-bg-not-applied: var(--color-cyan-800); + --event-button-bg-not-applied-hover: var(--color-cyan-900); + --event-button-bg-under-review: color-mix( + in oklab, + var(--color-cyan-700) 25%, + transparent + ); + --event-button-bg-under-review-hover: color-mix( + in oklab, + var(--color-cyan-700) 30%, + transparent + ); + --event-button-bg-accepted-attending: var(--color-green-700); + --event-button-bg-accepted-attending-hover: var(--color-green-800); + --event-button-bg-accepted-not-going: color-mix( + in oklab, + var(--color-neutral-200) 25%, + transparent + ); + --event-button-bg-accepted-not-going-hover: color-mix( + in oklab, + var(--color-neutral-200) 30%, + transparent + ); + --event-button-bg-rejected: color-mix( + in oklab, + var(--color-neutral-200) 25%, + transparent + ); + --event-button-bg-rejected-hover: color-mix( + in oklab, + var(--color-neutral-200) 30%, + transparent + ); + --event-button-bg-attending: color-mix( + in oklab, + var(--color-cyan-700) 25%, + transparent + ); + --event-button-bg-attending-hover: color-mix( + in oklab, + var(--color-cyan-700) 30%, + transparent + ); + --event-button-bg-waitlisted: #453a6b; + --event-button-bg-waitlisted-hover: #413569; + --event-button-bg-staff: color-mix( + in oklab, + var(--color-cyan-700) 25%, + transparent + ); + --event-button-bg-staff-hover: color-mix( + in oklab, + var(--color-cyan-700) 30%, + transparent + ); + --event-button-bg-admin: color-mix( + in oklab, + var(--color-cyan-700) 25%, + transparent + ); + --event-button-bg-admin-hover: color-mix( + in oklab, + var(--color-cyan-700) 30%, + transparent + ); + --event-button-bg-not-going: color-mix( + in oklab, + var(--color-neutral-200) 25%, + transparent + ); + --event-button-bg-not-going-hover: color-mix( + in oklab, + var(--color-neutral-200) 30%, + transparent + ); + --event-button-bg-completed: color-mix( + in oklab, + var(--color-neutral-200) 25%, + transparent + ); + --event-button-bg-completed-hover: color-mix( + in oklab, + var(--color-neutral-200) 30%, + transparent + ); + + --event-button-text-not-applied: var(--color-neutral-50); + --event-button-text-under-review: var(--color-cyan-500); + --event-button-text-accepted-attending: var(--color-neutral-50); + --event-button-text-accepted-not-going: var(--color-neutral-50); + --event-button-text-rejected: var(--color-neutral-50); + --event-button-text-attending: var(--color-cyan-500); + --event-button-text-waitlisted: var(--color-neutral-50); + --event-button-text-staff: var(--color-cyan-500); + --event-button-text-admin: var(--color-cyan-500); + --event-button-text-not-going: var(--color-neutral-50); + --event-button-text-completed: var(--color-neutral-50); } @theme inline { @@ -105,11 +275,11 @@ --color-badge-bg-attending: var(--badge-bg-attending); --color-badge-bg-accepted: var(--badge-bg-accepted); --color-badge-bg-waitlisted: var(--badge-bg-waitlisted); - --color-badge-bg-underReview: var(--badge-bg-underReview); - --color-badge-bg-notApplied: var(--badge-bg-notApplied); + --color-badge-bg-under-review: var(--badge-bg-under-review); + --color-badge-bg-not-applied: var(--badge-bg-not-applied); --color-badge-bg-staff: var(--badge-bg-staff); --color-badge-bg-admin: var(--badge-bg-admin); - --color-badge-bg-notGoing: var(--badge-bg-notGoing); + --color-badge-bg-not-going: var(--badge-bg-not-going); --color-badge-bg-completed: var(--badge-bg-completed); --color-badge-text-default: var(--badge-text-default); @@ -117,12 +287,71 @@ --color-badge-text-rejected: var(--badge-text-rejected); --color-badge-text-accepted: var(--badge-text-accepted); --color-badge-text-waitlisted: var(--badge-text-waitlisted); - --color-badge-text-underReview: var(--badge-text-underReview); - --color-badge-text-notApplied: var(--badge-text-notApplied); + --color-badge-text-under-review: var(--badge-text-under-review); + --color-badge-text-not-applied: var(--badge-text-not-applied); --color-badge-text-staff: var(--badge-text-staff); --color-badge-text-admin: var(--badge-text-admin); - --color-badge-text-notGoing: var(--badge-text-notGoing); + --color-badge-text-not-going: var(--badge-text-not-going); --color-badge-text-completed: var(--badge-text-completed); + --color-event-button-bg-not-applied: var(--event-button-bg-not-applied); + --color-event-button-bg-not-applied-hover: var( + --event-button-bg-not-applied-hover + ); + --color-event-button-bg-under-review: var(--event-button-bg-under-review); + --color-event-button-bg-under-review-hover: var( + --event-button-bg-under-review-hover + ); + --color-event-button-bg-accepted-attending: var( + --event-button-bg-accepted-attending + ); + --color-event-button-bg-accepted-attending-hover: var( + --event-button-bg-accepted-attending-hover + ); + --color-event-button-bg-accepted-not-going: var( + --event-button-bg-accepted-not-going + ); + --color-event-button-bg-accepted-not-going-hover: var( + --event-button-bg-accepted-not-going-hover + ); + --color-event-button-bg-rejected: var(--event-button-bg-rejected); + --color-event-button-bg-rejected-hover: var(--event-button-bg-rejected-hover); + --color-event-button-bg-attending: var(--event-button-bg-attending); + --color-event-button-bg-attending-hover: var( + --event-button-bg-attending-hover + ); + --color-event-button-bg-waitlisted: var(--event-button-bg-waitlisted); + --color-event-button-bg-waitlisted-hover: var( + --event-button-bg-waitlisted-hover + ); + --color-event-button-bg-staff: var(--event-button-bg-staff); + --color-event-button-bg-staff-hover: var(--event-button-bg-staff-hover); + --color-event-button-bg-admin: var(--event-button-bg-admin); + --color-event-button-bg-admin-hover: var(--event-button-bg-admin-hover); + --color-event-button-bg-not-going: var(--event-button-bg-not-going); + --color-event-button-bg-not-going-hover: var( + --event-button-bg-not-going-hover + ); + --color-event-button-bg-completed: var(--event-button-bg-completed); + --color-event-button-bg-completed-hover: var( + --event-button-bg-completed-hover + ); + + --color-event-button-text-not-applied: var(--event-button-text-not-applied); + --color-event-button-text-under-review: var(--event-button-text-under-review); + --color-event-button-text-accepted-attending: var( + --event-button-text-accepted-attending + ); + --color-event-button-text-accepted-not-going: var( + --event-button-text-accepted-not-going + ); + --color-event-button-text-rejected: var(--event-button-text-rejected); + --color-event-button-text-attending: var(--event-button-text-attending); + --color-event-button-text-waitlisted: var(--event-button-text-waitlisted); + --color-event-button-text-staff: var(--event-button-text-staff); + --color-event-button-text-admin: var(--event-button-text-admin); + --color-event-button-text-not-going: var(--event-button-text-not-going); + --color-event-button-text-completed: var(--event-button-text-completed); + --font-figtree: "Figtree", sans-serif; } From e6bb42b87b149c200edb84b4af08a3aebc587295 Mon Sep 17 00:00:00 2001 From: Hugo Liu <98724522+hugoliu-code@users.noreply.github.com> Date: Sun, 6 Jul 2025 20:56:44 -0700 Subject: [PATCH 20/52] TECH-125: Create Application Schema (#36) * Created Goose Migration for Applications * sqlc generate * improvement: updated fields * fix: updated env path * fix: syntax * chore: sqlc generate * feat: add more columns and auto updates + withdrawn status * chore: generate sqlc types for new schema * feat: indexes and default for application body --------- Co-authored-by: h1divp <71522316+h1divp@users.noreply.github.com> Co-authored-by: Alexander Wang --- .../20250621222955_create_applications.sql | 38 ++++++++++++ apps/api/internal/db/sqlc/models.go | 58 +++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 apps/api/internal/db/migrations/20250621222955_create_applications.sql diff --git a/apps/api/internal/db/migrations/20250621222955_create_applications.sql b/apps/api/internal/db/migrations/20250621222955_create_applications.sql new file mode 100644 index 00000000..0361b3ca --- /dev/null +++ b/apps/api/internal/db/migrations/20250621222955_create_applications.sql @@ -0,0 +1,38 @@ +-- +goose Up +-- +goose StatementBegin + +CREATE TYPE application_status AS ENUM ('started', 'submitted', 'under_review', 'accepted', 'rejected', 'waitlisted', 'withdrawn'); + +CREATE TABLE applications ( + user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE, + event_id UUID REFERENCES events(id) ON DELETE CASCADE, + status application_status DEFAULT 'started', + application JSONB NOT NULL DEFAULT '{}'::JSONB, + resume_url TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + saved_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + + PRIMARY KEY (user_id, event_id) +); + +CREATE INDEX idx_applications_status ON applications(status); +CREATE INDEX idx_applications_event_id ON applications(event_id); + +-- Create trigger to update application updates +CREATE TRIGGER set_updated_at_applications +BEFORE UPDATE ON applications +FOR EACH ROW +EXECUTE FUNCTION update_modified_column(); + +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin + + +DROP TRIGGER IF EXISTS set_updated_at_applications ON applications; +DROP TABLE IF EXISTS applications; +DROP TYPE IF EXISTS application_status; + +-- +goose StatementEnd diff --git a/apps/api/internal/db/sqlc/models.go b/apps/api/internal/db/sqlc/models.go index 148cb354..d149d0d1 100644 --- a/apps/api/internal/db/sqlc/models.go +++ b/apps/api/internal/db/sqlc/models.go @@ -12,6 +12,53 @@ import ( "github.com/google/uuid" ) +type ApplicationStatus string + +const ( + ApplicationStatusStarted ApplicationStatus = "started" + ApplicationStatusSubmitted ApplicationStatus = "submitted" + ApplicationStatusUnderReview ApplicationStatus = "under_review" + ApplicationStatusAccepted ApplicationStatus = "accepted" + ApplicationStatusRejected ApplicationStatus = "rejected" + ApplicationStatusWaitlisted ApplicationStatus = "waitlisted" + ApplicationStatusWithdrawn ApplicationStatus = "withdrawn" +) + +func (e *ApplicationStatus) Scan(src interface{}) error { + switch s := src.(type) { + case []byte: + *e = ApplicationStatus(s) + case string: + *e = ApplicationStatus(s) + default: + return fmt.Errorf("unsupported scan type for ApplicationStatus: %T", src) + } + return nil +} + +type NullApplicationStatus struct { + ApplicationStatus ApplicationStatus `json:"application_status"` + Valid bool `json:"valid"` // Valid is true if ApplicationStatus is not NULL +} + +// Scan implements the Scanner interface. +func (ns *NullApplicationStatus) Scan(value interface{}) error { + if value == nil { + ns.ApplicationStatus, ns.Valid = "", false + return nil + } + ns.Valid = true + return ns.ApplicationStatus.Scan(value) +} + +// Value implements the driver Valuer interface. +func (ns NullApplicationStatus) Value() (driver.Value, error) { + if !ns.Valid { + return nil, nil + } + return string(ns.ApplicationStatus), nil +} + type AuthUserRole string const ( @@ -98,6 +145,17 @@ func (ns NullEventRoleType) Value() (driver.Value, error) { return string(ns.EventRoleType), nil } +type Application struct { + UserID uuid.UUID `json:"user_id"` + EventID uuid.UUID `json:"event_id"` + Status NullApplicationStatus `json:"status"` + Application []byte `json:"application"` + ResumeUrl *string `json:"resume_url"` + CreatedAt time.Time `json:"created_at"` + SavedAt time.Time `json:"saved_at"` + UpdatedAt time.Time `json:"updated_at"` +} + type AuthAccount struct { ID uuid.UUID `json:"id"` UserID uuid.UUID `json:"user_id"` From 1ae7b0403f44345333c02fefb5c209db55624a25 Mon Sep 17 00:00:00 2001 From: Hugo Liu <98724522+hugoliu-code@users.noreply.github.com> Date: Tue, 8 Jul 2025 16:28:40 -0700 Subject: [PATCH 21/52] TECH-107: Add mailing list (#40) * Sqlc code and goose * Added mailing list functionality * ref: change to event interest submissions * fix: move to pgconn for error handling * fix: change to satisfy linter --------- Co-authored-by: Alexander Wang --- apps/api/cmd/api/main.go | 4 +- apps/api/go.mod | 1 + apps/api/internal/api/api.go | 28 +++---- .../internal/api/handlers/event_interest.go | 74 +++++++++++++++++++ apps/api/internal/api/handlers/handlers.go | 8 +- apps/api/internal/api/response/response.go | 16 ++++ apps/api/internal/db/errors.go | 15 ++++ .../20250627064521_create_mailing_list.sql | 16 ++++ .../db/queries/event_interest_submissions.sql | 12 +++ .../internal/db/repository/event_interest.go | 36 +++++++++ .../db/sqlc/event_interest_submissions.sql.go | 45 +++++++++++ apps/api/internal/db/sqlc/models.go | 8 ++ apps/api/internal/db/sqlc/querier.go | 4 + apps/api/internal/email/validation.go | 8 ++ apps/api/internal/services/event_interest.go | 47 ++++++++++++ 15 files changed, 305 insertions(+), 17 deletions(-) create mode 100644 apps/api/internal/api/handlers/event_interest.go create mode 100644 apps/api/internal/db/errors.go create mode 100644 apps/api/internal/db/migrations/20250627064521_create_mailing_list.sql create mode 100644 apps/api/internal/db/queries/event_interest_submissions.sql create mode 100644 apps/api/internal/db/repository/event_interest.go create mode 100644 apps/api/internal/db/sqlc/event_interest_submissions.sql.go create mode 100644 apps/api/internal/email/validation.go create mode 100644 apps/api/internal/services/event_interest.go diff --git a/apps/api/cmd/api/main.go b/apps/api/cmd/api/main.go index 18039efb..0d122dfc 100644 --- a/apps/api/cmd/api/main.go +++ b/apps/api/cmd/api/main.go @@ -45,12 +45,14 @@ func main() { userRepo := repository.NewUserRepository(database) accountRepo := repository.NewAccountRespository(database) sessionRepo := repository.NewSessionRepository(database) + eventInterestRepo := repository.NewEventInterestRepository(database) // Injections into services authService := services.NewAuthService(userRepo, accountRepo, sessionRepo, txm, client, logger, &cfg.Auth) + eventInterestService := services.NewEventInterestService(eventInterestRepo, logger) // Injections into handlers - apiHandlers := handlers.NewHandlers(authService, cfg, logger) + apiHandlers := handlers.NewHandlers(authService, eventInterestService, cfg, logger) api := api.NewAPI(&logger, apiHandlers, mw) diff --git a/apps/api/go.mod b/apps/api/go.mod index ff1187dd..73abb4b0 100644 --- a/apps/api/go.mod +++ b/apps/api/go.mod @@ -9,6 +9,7 @@ require ( github.com/google/uuid v1.6.0 github.com/jackc/pgx/v5 v5.7.4 github.com/joho/godotenv v1.5.1 + github.com/lib/pq v1.10.9 github.com/rs/zerolog v1.34.0 ) diff --git a/apps/api/internal/api/api.go b/apps/api/internal/api/api.go index 13f5ee24..b846840f 100644 --- a/apps/api/internal/api/api.go +++ b/apps/api/internal/api/api.go @@ -7,6 +7,7 @@ import ( "github.com/go-chi/chi/v5/middleware" "github.com/go-chi/cors" "github.com/rs/zerolog" + "github.com/rs/zerolog/log" "github.com/swamphacks/core/apps/api/internal/api/handlers" mw "github.com/swamphacks/core/apps/api/internal/api/middleware" "github.com/swamphacks/core/apps/api/internal/db/sqlc" @@ -44,55 +45,56 @@ func (api *API) setupRoutes(mw *mw.Middleware) { MaxAge: 300, })) + // Health check api.Router.Get("/ping", func(w http.ResponseWriter, r *http.Request) { api.Logger.Trace().Str("method", r.Method).Str("path", r.URL.Path).Msg("Received ping.") - w.Header().Set("Content-Type", "text/plain") w.Header().Set("Content-Length", "6") // "pong!\n" is 6 bytes - if _, err := w.Write([]byte("pong!\n")); err != nil { - return + log.Err(err) } - }) + // Auth routes api.Router.Route("/auth", func(r chi.Router) { r.Get("/callback", api.Handlers.Auth.OAuthCallback) r.Group(func(r chi.Router) { r.Use(mw.Auth.RequireAuth) - r.Get("/me", api.Handlers.Auth.GetMe) - r.Post("/logout", api.Handlers.Auth.Logout) }) }) - // Just for testing role perms right now + // Event routes + api.Router.Route("/event", func(r chi.Router) { + r.Post("/{eventId}/interest", api.Handlers.EventInterest.AddEmailToEvent) + }) + + // Protected test routes api.Router.Route("/protected", func(r chi.Router) { r.Use(mw.Auth.RequireAuth) + r.Get("/basic", func(w http.ResponseWriter, r *http.Request) { - if _, err := w.Write([]byte("Welcome, arbitrarily roled user that I don't know the role of yet!!\n")); err != nil { - return + if _, err := w.Write([]byte("Welcome, arbitrarily roled user!\n")); err != nil { + log.Err(err) } }) r.Group(func(r chi.Router) { r.Use(mw.Auth.RequirePlatformRole(sqlc.AuthUserRoleUser)) - r.Get("/user", func(w http.ResponseWriter, r *http.Request) { if _, err := w.Write([]byte("Welcome, user!\n")); err != nil { - return + log.Err(err) } }) }) r.Group(func(r chi.Router) { r.Use(mw.Auth.RequirePlatformRole(sqlc.AuthUserRoleSuperuser)) - r.Get("/superuser", func(w http.ResponseWriter, r *http.Request) { if _, err := w.Write([]byte("Welcome, superuser!\n")); err != nil { - return + log.Err(err) } }) }) diff --git a/apps/api/internal/api/handlers/event_interest.go b/apps/api/internal/api/handlers/event_interest.go new file mode 100644 index 00000000..de57af66 --- /dev/null +++ b/apps/api/internal/api/handlers/event_interest.go @@ -0,0 +1,74 @@ +package handlers + +import ( + "encoding/json" + "net/http" + + "github.com/go-chi/chi/v5" + "github.com/google/uuid" + "github.com/rs/zerolog" + res "github.com/swamphacks/core/apps/api/internal/api/response" + "github.com/swamphacks/core/apps/api/internal/config" + "github.com/swamphacks/core/apps/api/internal/email" + "github.com/swamphacks/core/apps/api/internal/services" +) + +type EventInterestHandler struct { + eventInterestService *services.EventInterestService + cfg *config.Config + logger zerolog.Logger +} + +func NewEventInterestHandler(eventInterestService *services.EventInterestService, cfg *config.Config, logger zerolog.Logger) *EventInterestHandler { + return &EventInterestHandler{ + eventInterestService: eventInterestService, + cfg: cfg, + logger: logger.With().Str("handler", "EventInterestHandler").Str("component", "event_interest").Logger(), + } +} + +// AddEmailRequest is the expected payload for adding an email +type AddEmailRequest struct { + Email string `json:"email"` + Source *string `json:"source"` +} + +func (h *EventInterestHandler) AddEmailToEvent(w http.ResponseWriter, r *http.Request) { + eventIdStr := chi.URLParam(r, "eventId") + if eventIdStr == "" { + res.SendError(w, http.StatusBadRequest, res.NewError("missing_event", "The event ID is missing from the URL!")) + return + } + eventId, err := uuid.Parse(eventIdStr) + if err != nil { + res.SendError(w, http.StatusBadRequest, res.NewError("invalid_event_id", "The event ID is not a valid UUID")) + return + } + + // Parse JSON body + var req AddEmailRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + res.SendError(w, http.StatusBadRequest, res.NewError("invalid_request", "Could not parse request body")) + return + } + + if !email.IsValidEmail(req.Email) { + res.SendError(w, http.StatusBadRequest, res.NewError("missing_email", "Email is required")) + return + } + + _, err = h.eventInterestService.CreateInterestSubmission(r.Context(), eventId, req.Email, req.Source) + if err != nil { + switch err { + case services.ErrEmailConflict: + res.SendError(w, http.StatusConflict, res.NewError("duplicate_email", "Email is already registered for this event")) + case services.ErrFailedToCreateSubmission: + res.SendError(w, http.StatusInternalServerError, res.NewError("submission_error", "Failed to create event interest submission")) + default: + res.SendError(w, http.StatusInternalServerError, res.NewError("internal_err", "Something went wrong")) + } + return + } + + w.WriteHeader(http.StatusCreated) +} diff --git a/apps/api/internal/api/handlers/handlers.go b/apps/api/internal/api/handlers/handlers.go index cd1dd822..52d0d1ef 100644 --- a/apps/api/internal/api/handlers/handlers.go +++ b/apps/api/internal/api/handlers/handlers.go @@ -7,11 +7,13 @@ import ( ) type Handlers struct { - Auth *AuthHandler + Auth *AuthHandler + EventInterest *EventInterestHandler } -func NewHandlers(authService *services.AuthService, cfg *config.Config, logger zerolog.Logger) *Handlers { +func NewHandlers(authService *services.AuthService, eventInterestService *services.EventInterestService, cfg *config.Config, logger zerolog.Logger) *Handlers { return &Handlers{ - Auth: NewAuthHandler(authService, cfg, logger), + Auth: NewAuthHandler(authService, cfg, logger), + EventInterest: NewEventInterestHandler(eventInterestService, cfg, logger), } } diff --git a/apps/api/internal/api/response/response.go b/apps/api/internal/api/response/response.go index f4095478..39089c67 100644 --- a/apps/api/internal/api/response/response.go +++ b/apps/api/internal/api/response/response.go @@ -28,3 +28,19 @@ func SendError(w http.ResponseWriter, status int, errorResponse ErrorResponse) { http.Error(w, "Internal server error", http.StatusInternalServerError) } } + +// Send marshals any successful payload struct to JSON, sets the status code, +// and writes the response. +func Send(w http.ResponseWriter, status int, payload interface{}) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(status) + + if payload != nil { + if err := json.NewEncoder(w).Encode(payload); err != nil { + // If encoding fails, log the error and fall back to a plain text error. + // This is crucial because the header has already been written. + log.Err(err).Str("function", "Send").Msg("Failed to encode and send JSON success object") + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + } + } +} diff --git a/apps/api/internal/db/errors.go b/apps/api/internal/db/errors.go new file mode 100644 index 00000000..28dceda4 --- /dev/null +++ b/apps/api/internal/db/errors.go @@ -0,0 +1,15 @@ +package db + +import ( + "errors" + + "github.com/jackc/pgx/v5/pgconn" +) + +func IsUniqueViolation(err error) bool { + var pgErr *pgconn.PgError + if errors.As(err, &pgErr) { + return pgErr.Code == "23505" + } + return false +} diff --git a/apps/api/internal/db/migrations/20250627064521_create_mailing_list.sql b/apps/api/internal/db/migrations/20250627064521_create_mailing_list.sql new file mode 100644 index 00000000..ad958756 --- /dev/null +++ b/apps/api/internal/db/migrations/20250627064521_create_mailing_list.sql @@ -0,0 +1,16 @@ +-- +goose Up +CREATE TABLE event_interest_submissions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + event_id UUID NOT NULL REFERENCES events(id) ON DELETE CASCADE, + email TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + source TEXT +); + +CREATE INDEX idx_event_interest_event_id ON event_interest_submissions (event_id); +CREATE UNIQUE INDEX uniq_event_email ON event_interest_submissions (event_id, email); + +-- +goose Down +DROP INDEX IF EXISTS uniq_event_email; +DROP INDEX IF EXISTS idx_event_interest_event_id; +DROP TABLE IF EXISTS event_interest_submissions; diff --git a/apps/api/internal/db/queries/event_interest_submissions.sql b/apps/api/internal/db/queries/event_interest_submissions.sql new file mode 100644 index 00000000..475bf777 --- /dev/null +++ b/apps/api/internal/db/queries/event_interest_submissions.sql @@ -0,0 +1,12 @@ +-- name: AddEmail :one +-- Adds a new email to the mailing list for a specific user and event. +-- The unique constraint on (event_id, user_id) will prevent duplicates. +-- Returns the newly created email record. +INSERT INTO event_interest_submissions ( + event_id, + email, + source +) VALUES ( + $1, $2, $3 +) +RETURNING *; \ No newline at end of file diff --git a/apps/api/internal/db/repository/event_interest.go b/apps/api/internal/db/repository/event_interest.go new file mode 100644 index 00000000..886ae9fc --- /dev/null +++ b/apps/api/internal/db/repository/event_interest.go @@ -0,0 +1,36 @@ +package repository + +import ( + "context" + "errors" + + "github.com/swamphacks/core/apps/api/internal/db" + "github.com/swamphacks/core/apps/api/internal/db/sqlc" +) + +var ( + ErrDuplicateEmails = errors.New("email already exists in the database") +) + +type EventInterestRepository struct { + db *db.DB +} + +func NewEventInterestRepository(db *db.DB) *EventInterestRepository { + return &EventInterestRepository{ + db: db, + } +} + +func (r *EventInterestRepository) AddEmail(ctx context.Context, params sqlc.AddEmailParams) (*sqlc.EventInterestSubmission, error) { + interestSubmission, err := r.db.Query.AddEmail(ctx, params) + if err != nil { + if db.IsUniqueViolation(err) { + return nil, ErrDuplicateEmails + } + + return nil, err + } + + return &interestSubmission, nil +} diff --git a/apps/api/internal/db/sqlc/event_interest_submissions.sql.go b/apps/api/internal/db/sqlc/event_interest_submissions.sql.go new file mode 100644 index 00000000..27239b85 --- /dev/null +++ b/apps/api/internal/db/sqlc/event_interest_submissions.sql.go @@ -0,0 +1,45 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 +// source: event_interest_submissions.sql + +package sqlc + +import ( + "context" + + "github.com/google/uuid" +) + +const addEmail = `-- name: AddEmail :one +INSERT INTO event_interest_submissions ( + event_id, + email, + source +) VALUES ( + $1, $2, $3 +) +RETURNING id, event_id, email, created_at, source +` + +type AddEmailParams struct { + EventID uuid.UUID `json:"event_id"` + Email string `json:"email"` + Source *string `json:"source"` +} + +// Adds a new email to the mailing list for a specific user and event. +// The unique constraint on (event_id, user_id) will prevent duplicates. +// Returns the newly created email record. +func (q *Queries) AddEmail(ctx context.Context, arg AddEmailParams) (EventInterestSubmission, error) { + row := q.db.QueryRow(ctx, addEmail, arg.EventID, arg.Email, arg.Source) + var i EventInterestSubmission + err := row.Scan( + &i.ID, + &i.EventID, + &i.Email, + &i.CreatedAt, + &i.Source, + ) + return i, err +} diff --git a/apps/api/internal/db/sqlc/models.go b/apps/api/internal/db/sqlc/models.go index d149d0d1..159ac243 100644 --- a/apps/api/internal/db/sqlc/models.go +++ b/apps/api/internal/db/sqlc/models.go @@ -214,6 +214,14 @@ type Event struct { UpdatedAt *time.Time `json:"updated_at"` } +type EventInterestSubmission struct { + ID uuid.UUID `json:"id"` + EventID uuid.UUID `json:"event_id"` + Email string `json:"email"` + CreatedAt time.Time `json:"created_at"` + Source *string `json:"source"` +} + type EventRole struct { UserID uuid.UUID `json:"user_id"` EventID uuid.UUID `json:"event_id"` diff --git a/apps/api/internal/db/sqlc/querier.go b/apps/api/internal/db/sqlc/querier.go index d3266879..b649a3a3 100644 --- a/apps/api/internal/db/sqlc/querier.go +++ b/apps/api/internal/db/sqlc/querier.go @@ -11,6 +11,10 @@ import ( ) type Querier interface { + // Adds a new email to the mailing list for a specific user and event. + // The unique constraint on (event_id, user_id) will prevent duplicates. + // Returns the newly created email record. + AddEmail(ctx context.Context, arg AddEmailParams) (EventInterestSubmission, error) CreateAccount(ctx context.Context, arg CreateAccountParams) (AuthAccount, error) CreateEvent(ctx context.Context, arg CreateEventParams) (Event, error) CreateSession(ctx context.Context, arg CreateSessionParams) (AuthSession, error) diff --git a/apps/api/internal/email/validation.go b/apps/api/internal/email/validation.go new file mode 100644 index 00000000..c1ce5a8e --- /dev/null +++ b/apps/api/internal/email/validation.go @@ -0,0 +1,8 @@ +package email + +import "net/mail" + +func IsValidEmail(email string) bool { + _, err := mail.ParseAddress(email) + return err == nil +} diff --git a/apps/api/internal/services/event_interest.go b/apps/api/internal/services/event_interest.go new file mode 100644 index 00000000..fadb7929 --- /dev/null +++ b/apps/api/internal/services/event_interest.go @@ -0,0 +1,47 @@ +package services + +import ( + "context" + "errors" + + "github.com/google/uuid" + "github.com/rs/zerolog" + "github.com/swamphacks/core/apps/api/internal/db/repository" + "github.com/swamphacks/core/apps/api/internal/db/sqlc" +) + +var ( + ErrEmailConflict = errors.New("email already exists in this mailing list") + ErrFailedToCreateSubmission = errors.New("failed to create event interest submission") +) + +type EventInterestService struct { + eventInterestRepo *repository.EventInterestRepository + logger zerolog.Logger +} + +func NewEventInterestService(eventInterestRepo *repository.EventInterestRepository, logger zerolog.Logger) *EventInterestService { + return &EventInterestService{ + eventInterestRepo: eventInterestRepo, + logger: logger.With().Str("service", "EventInterestService").Str("component", "event_interest").Logger(), + } +} + +func (s *EventInterestService) CreateInterestSubmission(ctx context.Context, eventID uuid.UUID, email string, source *string) (*sqlc.EventInterestSubmission, error) { + params := sqlc.AddEmailParams{ + EventID: eventID, + Email: email, + Source: source, + } + + result, err := s.eventInterestRepo.AddEmail(ctx, params) + if err != nil && err == repository.ErrDuplicateEmails { + s.logger.Err(err).Msg("Could not insert email due to duplicate existing.") + return nil, ErrEmailConflict + } else if err != nil { + s.logger.Err(err).Msg("An unknown error was caught!") + return nil, ErrFailedToCreateSubmission + } + + return result, nil +} From 1948d296e2876be113bc6044d07b9e691818a029 Mon Sep 17 00:00:00 2001 From: Alexander Wang <98280966+AlexanderWangY@users.noreply.github.com> Date: Wed, 9 Jul 2025 10:37:30 -0400 Subject: [PATCH 22/52] docs(swagger): updated swagger and regenerated web types for event interest (#50) --- apps/web/src/lib/openapi/schema.d.ts | 76 ++++++++++++++++++++++++++++ shared/openapi/core-api.yaml | 66 ++++++++++++++++++++++++ 2 files changed, 142 insertions(+) diff --git a/apps/web/src/lib/openapi/schema.d.ts b/apps/web/src/lib/openapi/schema.d.ts index c963d7c5..4354f6dd 100644 --- a/apps/web/src/lib/openapi/schema.d.ts +++ b/apps/web/src/lib/openapi/schema.d.ts @@ -44,6 +44,23 @@ export interface paths { patch?: never; trace?: never; }; + "/event/{eventId}/interest": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Make an interest submission for an event (email list) */ + post: operations["post-event-interest"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; } export type webhooks = Record; export interface components { @@ -226,4 +243,63 @@ export interface operations { }; }; }; + "post-event-interest": { + parameters: { + query?: never; + header?: never; + path: { + eventId: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** + * Format: email + * @example johndoe@ufl.edu + */ + email: string; + /** @example SHX Frontpage */ + source?: string; + }; + }; + }; + responses: { + /** @description OK: Interest email created */ + 201: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Bad request/Malformed request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Duplicate email found in DB */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Server Error: Something went terribly wrong on our end. */ + "5XX": { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; } diff --git a/shared/openapi/core-api.yaml b/shared/openapi/core-api.yaml index b045b5a7..03bc1e19 100644 --- a/shared/openapi/core-api.yaml +++ b/shared/openapi/core-api.yaml @@ -165,6 +165,72 @@ paths: description: The authenticated session token/id tags: - Authentication + /event/{eventId}/interest: + post: + summary: Make an interest submission for an event (email list) + parameters: + - name: eventId + in: path + required: true + schema: + type: string + format: uuid + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - email + properties: + email: + type: string + format: email + example: johndoe@ufl.edu + source: + type: string + example: "SHX Frontpage" + responses: + '201': + description: 'OK: Interest email created' + '400': + description: 'Bad request/Malformed request' + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + Example 1: + value: + error: missing_email + message: Email is required + '409': + description: 'Duplicate email found in DB' + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + Example 1: + value: + error: duplicate_email + message: Email is already registered for this event + + 5XX: + description: 'Server Error: Something went terribly wrong on our end.' + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + Example 1: + value: + error: internal_err + message: Something went wrong on our end. + operationId: post-event-interest + tags: + - Event components: schemas: ErrorResponse: From 09383f70f862996730e7d0f741020deff51b4347 Mon Sep 17 00:00:00 2001 From: Alexander Wang <98280966+AlexanderWangY@users.noreply.github.com> Date: Thu, 10 Jul 2025 16:06:00 -0500 Subject: [PATCH 23/52] Navlink component and some page scaffolding (#51) * feat: navlink component and storyboard * feat: expandable links! * feat: polished navlink component * chore: removed unused colors and replaced colors --------- Co-authored-by: Alex Wang --- apps/web/package.json | 2 + apps/web/pnpm-lock.yaml | 6 + apps/web/src/components/AppShell/NavLink.tsx | 110 ++++++++++++++++++ .../AppShell/stories/NavLink.stories.tsx | 84 +++++++++++++ apps/web/src/index.css | 4 +- apps/web/src/routes/_protected/community.tsx | 9 ++ apps/web/src/routes/_protected/layout.tsx | 67 ++++++++++- .../_protected/resources/programming.tsx | 9 ++ .../routes/_protected/resources/sponsors.tsx | 9 ++ apps/web/src/theme.css | 21 ++++ 10 files changed, 317 insertions(+), 4 deletions(-) create mode 100644 apps/web/src/components/AppShell/NavLink.tsx create mode 100644 apps/web/src/components/AppShell/stories/NavLink.stories.tsx create mode 100644 apps/web/src/routes/_protected/community.tsx create mode 100644 apps/web/src/routes/_protected/resources/programming.tsx create mode 100644 apps/web/src/routes/_protected/resources/sponsors.tsx diff --git a/apps/web/package.json b/apps/web/package.json index f84c0e5c..a2ef1782 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -36,6 +36,7 @@ "react": "^19.1.0", "react-aria-components": "^1.8.0", "react-dom": "^19.1.0", + "react-stately": "^3.39.0", "tailwind-merge": "^3.3.0", "tailwind-variants": "^1.0.0", "tailwindcss": "^4.1.5", @@ -55,6 +56,7 @@ "@storybook/react": "^8.6.12", "@storybook/react-vite": "^8.6.12", "@storybook/test": "^8.6.12", + "@storybook/theming": "^8.6.14", "@svgr/core": "^8.1.0", "@svgr/plugin-jsx": "^8.1.0", "@tanstack/router-plugin": "^1.120.2", diff --git a/apps/web/pnpm-lock.yaml b/apps/web/pnpm-lock.yaml index ccc42f3f..2c17d3d5 100644 --- a/apps/web/pnpm-lock.yaml +++ b/apps/web/pnpm-lock.yaml @@ -38,6 +38,9 @@ importers: react-dom: specifier: ^19.1.0 version: 19.1.0(react@19.1.0) + react-stately: + specifier: ^3.39.0 + version: 3.39.0(react@19.1.0) tailwind-merge: specifier: ^3.3.0 version: 3.3.1 @@ -90,6 +93,9 @@ importers: '@storybook/test': specifier: ^8.6.12 version: 8.6.14(storybook@8.6.14(prettier@3.5.3)) + '@storybook/theming': + specifier: ^8.6.14 + version: 8.6.14(storybook@8.6.14(prettier@3.5.3)) '@svgr/core': specifier: ^8.1.0 version: 8.1.0(typescript@5.8.3) diff --git a/apps/web/src/components/AppShell/NavLink.tsx b/apps/web/src/components/AppShell/NavLink.tsx new file mode 100644 index 00000000..d005d48e --- /dev/null +++ b/apps/web/src/components/AppShell/NavLink.tsx @@ -0,0 +1,110 @@ +import type { PropsWithChildren, ReactNode } from "react"; +import TablerChevronRight from "~icons/tabler/chevron-right"; +import { tv } from "tailwind-variants"; +import { useToggleState } from "react-stately"; +import { cn } from "@/utils/cn"; +import { Button as RAC_Button } from "react-aria-components"; +import { Link } from "@tanstack/react-router"; + +const navLink = tv({ + base: "px-3 py-2.5 rounded-sm text-sm flex flex-row items-center justify-between w-full cursor-pointer transition-none select-none text-navlink-text", + variants: { + active: { + true: "bg-navlink-bg-active font-medium", + false: "bg-navlink-bg-inactive font-normal hover:scale-101", + }, + }, +}); + +interface NavLinkProps { + href?: string; + label: string; + description?: string; + leftSection?: ReactNode; + rightSection?: ReactNode; + active?: boolean; + initialExpanded?: boolean; +} + +const NavLink = ({ + href, + label, + description, + leftSection, + rightSection, + active = false, + initialExpanded = false, + children, +}: PropsWithChildren) => { + const isExpandable = !!children; + + const toggleState = useToggleState({ defaultSelected: initialExpanded }); + const toggle = toggleState.toggle; + + return ( +
+ {isExpandable ? ( + +
+ {leftSection && ( + + {leftSection} + + )} +
+ {label} + {description && ( + + {description} + + )} +
+
+ + + + +
+ ) : ( + +
+ {leftSection && ( + + {leftSection} + + )} +
+ {label} + {description && ( + + {description} + + )} +
+
+ {rightSection && ( + + {rightSection} + + )} + + )} + + {/* Expanded children links */} +
+ {isExpandable && toggleState.isSelected && ( +
{children}
+ )} +
+
+ ); +}; + +NavLink.displayName = "NavLink"; + +export { NavLink }; diff --git a/apps/web/src/components/AppShell/stories/NavLink.stories.tsx b/apps/web/src/components/AppShell/stories/NavLink.stories.tsx new file mode 100644 index 00000000..15cfe999 --- /dev/null +++ b/apps/web/src/components/AppShell/stories/NavLink.stories.tsx @@ -0,0 +1,84 @@ +/* This is bugged out brother */ +import type { Meta, StoryObj } from "@storybook/react"; +import TablerLayoutCollage from "~icons/tabler/layout-collage"; +import { NavLink } from "../NavLink"; +import { + createRootRouteWithContext, + createRouter, + RouterProvider, +} from "@tanstack/react-router"; +import type { ReactNode } from "react"; + +// Define a dummy root component that wraps Story +function withTanstackRouter(StoryComponent: () => ReactNode) { + const RootRoute = createRootRouteWithContext()({ + component: StoryComponent, + }); + + const router = createRouter({ + routeTree: RootRoute, + }); + + return ; +} + +const meta = { + component: NavLink, + title: "AppShell/NavLink", + tags: ["autodocs"], + decorators: [(Story) => withTanstackRouter(Story)], + argTypes: { + href: { + control: { type: "text" }, + description: "The URL the link points to", + defaultValue: "https://core.apidocumentation.com", + }, + label: { + control: { type: "text" }, + description: "The text displayed in the link", + defaultValue: "Dashboard", + }, + active: { + control: { type: "boolean" }, + description: "Indicates if the link is active", + defaultValue: false, + }, + }, + args: { + href: "https://core.apidocumentation.com", + label: "Dashboard", + active: true, + leftSection: ( + + ), + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + label: "Dashboard", + href: "https://core.apidocumentation.com", + }, +}; + +export const Expandable: Story = { + args: { + label: "Mother", + children: ( + <> + + + + ), + }, +}; + +export const WithDescription: Story = { + args: { + label: "Resume Review", + description: "67/67 (100%) resumes reviewed.", + }, +}; diff --git a/apps/web/src/index.css b/apps/web/src/index.css index b1bf9bbe..f2b2715d 100644 --- a/apps/web/src/index.css +++ b/apps/web/src/index.css @@ -18,6 +18,6 @@ body, } /* https://stackoverflow.com/questions/68400074/how-to-apply-transition-effects-when-switching-from-light-mode-to-dark-in-tailwi */ -body.enable-theme-transition * { - @apply transition-colors duration-500; +body.enable-theme-transition { + @apply transition-colors duration-300; } diff --git a/apps/web/src/routes/_protected/community.tsx b/apps/web/src/routes/_protected/community.tsx new file mode 100644 index 00000000..d9d0dbf0 --- /dev/null +++ b/apps/web/src/routes/_protected/community.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute("/_protected/community")({ + component: RouteComponent, +}); + +function RouteComponent() { + return
Hello this is the community tab!
; +} diff --git a/apps/web/src/routes/_protected/layout.tsx b/apps/web/src/routes/_protected/layout.tsx index efd0724e..5ae03849 100644 --- a/apps/web/src/routes/_protected/layout.tsx +++ b/apps/web/src/routes/_protected/layout.tsx @@ -1,4 +1,13 @@ -import { createFileRoute, Outlet, redirect } from "@tanstack/react-router"; +import { NavLink } from "@/components/AppShell/NavLink"; +import { + createFileRoute, + Outlet, + redirect, + useLocation, +} from "@tanstack/react-router"; +import TablerLayoutCollage from "~icons/tabler/layout-collage"; +import TablerBooks from "~icons/tabler/books"; +import TablerSocial from "~icons/tabler/social"; // This layout component performs authentication checks before the user can access protected pages export const Route = createFileRoute("/_protected")({ @@ -34,5 +43,59 @@ export const Route = createFileRoute("/_protected")({ }); function RouteComponent() { - return ; + const pathname = useLocation({ select: (loc) => loc.pathname }); + + return ( +
+
+

SwampHacks

+
+ +
+ + + {/* Main content */} +
+ +
+
+
+ ); } diff --git a/apps/web/src/routes/_protected/resources/programming.tsx b/apps/web/src/routes/_protected/resources/programming.tsx new file mode 100644 index 00000000..477d6485 --- /dev/null +++ b/apps/web/src/routes/_protected/resources/programming.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute("/_protected/resources/programming")({ + component: RouteComponent, +}); + +function RouteComponent() { + return
Hello "/_protected/resources/programming"!
; +} diff --git a/apps/web/src/routes/_protected/resources/sponsors.tsx b/apps/web/src/routes/_protected/resources/sponsors.tsx new file mode 100644 index 00000000..ed094ab0 --- /dev/null +++ b/apps/web/src/routes/_protected/resources/sponsors.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute("/_protected/resources/sponsors")({ + component: RouteComponent, +}); + +function RouteComponent() { + return
Hello "/_protected/resources/sponsors"!
; +} diff --git a/apps/web/src/theme.css b/apps/web/src/theme.css index c42452d2..99566bbe 100644 --- a/apps/web/src/theme.css +++ b/apps/web/src/theme.css @@ -8,6 +8,14 @@ --text-main: var(--color-zinc-900); --text-secondary: var(--color-zinc-600); + /* NAVBAR */ + --navlink-bg-inactive: transparent; + --navlink-bg-active: var(--color-neutral-300); + + /* NAVBAR TEXT */ + --navlink-text: var(--text-main); + --navlink-secondary-text: var(--color-neutral-500); + /* BUTTONS */ --button-primary: var(--color-blue-500); --button-primary-hover: var(--color-blue-700); @@ -120,6 +128,14 @@ --text-main: var(--color-zinc-100); --text-secondary: var(--color-zinc-400); + /* NAVBAR */ + --navlink-bg-inactive: transparent; + --navlink-bg-active: var(--surface); + + /* NAVBAR TEXT */ + --navlink-text: var(--text-main); + --navlink-secondary-text: var(--color-neutral-500); + /* BUTTONS */ --button-primary: var(--color-blue-500); --button-primary-hover: var(--color-blue-700); @@ -263,6 +279,11 @@ --color-text-main: var(--text-main); --color-text-secondary: var(--text-secondary); + --color-navlink-bg-inactive: var(--navlink-bg-inactive); + --color-navlink-bg-active: var(--navlink-bg-active); + --color-navlink-text: var(--navlink-text); + --color-navlink-secondary-text: var(--navlink-secondary-text); + --color-button-primary: var(--button-primary); --color-button-primary-hover: var(--button-primary-hover); --color-button-primary-disabled: var(--button-primary-disabled); From 217eb182fd52ab0363815515894fd0bf97e316e6 Mon Sep 17 00:00:00 2001 From: Alexander Wang <98280966+AlexanderWangY@users.noreply.github.com> Date: Fri, 11 Jul 2025 15:00:37 -0500 Subject: [PATCH 24/52] [TECH 113] Navbar + Appshell beta (#52) * feat: navlink component and storyboard * feat: expandable links! * feat: polished navlink component * chore: removed unused colors and replaced colors * feat: mobile nav + Appshell * chore: clean up unused comments --------- Co-authored-by: Alex Wang --- apps/web/src/components/AppShell/AppShell.tsx | 100 ++++++++++++++++++ .../components/AppShell/AppShellContext.tsx | 17 +++ .../src/components/AppShell/MobileSidebar.tsx | 33 ++++++ apps/web/src/components/AppShell/NavLink.tsx | 14 ++- apps/web/src/routes/_protected/layout.tsx | 94 ++++++++-------- 5 files changed, 209 insertions(+), 49 deletions(-) create mode 100644 apps/web/src/components/AppShell/AppShell.tsx create mode 100644 apps/web/src/components/AppShell/AppShellContext.tsx create mode 100644 apps/web/src/components/AppShell/MobileSidebar.tsx diff --git a/apps/web/src/components/AppShell/AppShell.tsx b/apps/web/src/components/AppShell/AppShell.tsx new file mode 100644 index 00000000..95f0e0f1 --- /dev/null +++ b/apps/web/src/components/AppShell/AppShell.tsx @@ -0,0 +1,100 @@ +import { + Children, + isValidElement, + memo, + useMemo, + type FC, + type PropsWithChildren, + type ReactNode, +} from "react"; +import { SlideoutNavbar } from "./MobileSidebar"; +import { AppShellContext } from "./AppShellContext"; +import { useToggleState } from "react-stately"; +import { Button } from "react-aria-components"; +import TablerMenu2 from "~icons/tabler/menu-2"; +import IconX from "~icons/tabler/x"; + +interface AppShellComponent extends FC { + Header: FC; + Navbar: FC; + Main: FC; +} + +function extractAppShellChildren(children: ReactNode) { + let header: ReactNode = null; + let navbar: ReactNode = null; + let main: ReactNode = null; + + Children.forEach(children, (child) => { + if (!isValidElement(child)) return; + + if (child.type === AppShell.Header) header = child; + else if (child.type === AppShell.Navbar) navbar = child; + else if (child.type === AppShell.Main) main = child; + }); + + return { header, navbar, main }; +} + +const AppShellBase: FC = ({ children }) => { + const { toggle, isSelected, setSelected } = useToggleState(); + const { header, navbar, main } = useMemo( + () => extractAppShellChildren(children), + [children], + ); + + return ( + +
+ {/* Topbar */} + {header && ( +
+ {/* Mobile Burger menu */} + + {header} +
+ )} + +
+ {/* Desktop Sidebar */} + {navbar && ( + + )} + + {/* Slideout for Mobile */} + {navbar} + + {/* Main content */} + {main &&
{main}
} +
+
+
+ ); +}; + +const AppShell = memo(AppShellBase) as unknown as AppShellComponent; + +AppShell.Header = memo(({ children }: PropsWithChildren) => <>{children}); +AppShell.Navbar = memo(({ children }: PropsWithChildren) => <>{children}); +AppShell.Main = memo(({ children }: PropsWithChildren) => <>{children}); + +AppShell.displayName = "AppShell"; +AppShell.Header.displayName = "AppShell.Header"; +AppShell.Navbar.displayName = "AppShell.Navbar"; +AppShell.Main.displayName = "AppShell.Main"; + +export { AppShell }; diff --git a/apps/web/src/components/AppShell/AppShellContext.tsx b/apps/web/src/components/AppShell/AppShellContext.tsx new file mode 100644 index 00000000..c991938a --- /dev/null +++ b/apps/web/src/components/AppShell/AppShellContext.tsx @@ -0,0 +1,17 @@ +import { createContext, useContext } from "react"; + +export interface AppShellContextValue { + isMobileNavOpen: boolean; + toggleMobileNav: () => void; + setMobileNavOpen: (open: boolean) => void; +} + +export const AppShellContext = createContext( + undefined, +); + +export const useAppShell = () => { + const ctx = useContext(AppShellContext); + if (!ctx) throw new Error("useAppShell must be used within "); + return ctx; +}; diff --git a/apps/web/src/components/AppShell/MobileSidebar.tsx b/apps/web/src/components/AppShell/MobileSidebar.tsx new file mode 100644 index 00000000..2fc570e4 --- /dev/null +++ b/apps/web/src/components/AppShell/MobileSidebar.tsx @@ -0,0 +1,33 @@ +import type { PropsWithChildren } from "react"; +import { useAppShell } from "./AppShellContext"; + +interface MobileSidebarProps extends PropsWithChildren { + isOpen: boolean; +} + +const SlideoutNavbar = ({ children, isOpen }: MobileSidebarProps) => { + const { setMobileNavOpen } = useAppShell(); + + return ( + <> + {/* Overlay */} +
setMobileNavOpen(false)} + className={`fixed top-16 left-0 right-0 bottom-0 transition-colors duration-300 ease-in-out z-40 ${ + isOpen ? "bg-black/20" : "bg-black/0 pointer-events-none" + }`} + /> + + {/* Side Navigation */} + + + ); +}; + +export { SlideoutNavbar }; diff --git a/apps/web/src/components/AppShell/NavLink.tsx b/apps/web/src/components/AppShell/NavLink.tsx index d005d48e..d91cc73c 100644 --- a/apps/web/src/components/AppShell/NavLink.tsx +++ b/apps/web/src/components/AppShell/NavLink.tsx @@ -5,9 +5,10 @@ import { useToggleState } from "react-stately"; import { cn } from "@/utils/cn"; import { Button as RAC_Button } from "react-aria-components"; import { Link } from "@tanstack/react-router"; +import { useAppShell } from "./AppShellContext"; const navLink = tv({ - base: "px-3 py-2.5 rounded-sm text-sm flex flex-row items-center justify-between w-full cursor-pointer transition-none select-none text-navlink-text", + base: "px-3 py-2.5 rounded-sm text-md flex flex-row items-center justify-between w-full cursor-pointer transition-none select-none text-navlink-text", variants: { active: { true: "bg-navlink-bg-active font-medium", @@ -24,6 +25,7 @@ interface NavLinkProps { rightSection?: ReactNode; active?: boolean; initialExpanded?: boolean; + closeNavbarOnClick?: boolean; } const NavLink = ({ @@ -34,6 +36,7 @@ const NavLink = ({ rightSection, active = false, initialExpanded = false, + closeNavbarOnClick = true, children, }: PropsWithChildren) => { const isExpandable = !!children; @@ -41,6 +44,9 @@ const NavLink = ({ const toggleState = useToggleState({ defaultSelected: initialExpanded }); const toggle = toggleState.toggle; + // Handle mobile navigation state in tangent with the AppShell context + const { setMobileNavOpen } = useAppShell(); + return (
{isExpandable ? ( @@ -71,7 +77,11 @@ const NavLink = ({ ) : ( - + (closeNavbarOnClick ? setMobileNavOpen(false) : null)} + >
{leftSection && ( diff --git a/apps/web/src/routes/_protected/layout.tsx b/apps/web/src/routes/_protected/layout.tsx index 5ae03849..3bf4e9d4 100644 --- a/apps/web/src/routes/_protected/layout.tsx +++ b/apps/web/src/routes/_protected/layout.tsx @@ -8,6 +8,7 @@ import { import TablerLayoutCollage from "~icons/tabler/layout-collage"; import TablerBooks from "~icons/tabler/books"; import TablerSocial from "~icons/tabler/social"; +import { AppShell } from "@/components/AppShell/AppShell"; // This layout component performs authentication checks before the user can access protected pages export const Route = createFileRoute("/_protected")({ @@ -46,56 +47,55 @@ function RouteComponent() { const pathname = useLocation({ select: (loc) => loc.pathname }); return ( -
-
-

SwampHacks

-
+ + +
+

SwampHacks

-
-
+ - } - > - - - + + } + active={pathname.startsWith("/dashboard")} + /> - } - active={pathname.startsWith("/community")} - /> - - + } + > + + + - {/* Main content */} -
- -
-
-
+ } + active={pathname.startsWith("/community")} + /> + + + + + + ); } From 650bc03a36f944726f2ef67b6bc98bb9f4eb9b9a Mon Sep 17 00:00:00 2001 From: Alexander Wang <98280966+AlexanderWangY@users.noreply.github.com> Date: Fri, 11 Jul 2025 16:17:31 -0500 Subject: [PATCH 25/52] feat: admin and event page setup (#53) --- apps/web/src/routes/_protected/layout.tsx | 6 +- .../_protected/{dashboard.tsx => portal.tsx} | 5 +- .../src/routes/admin/events-management.tsx | 9 ++ apps/web/src/routes/admin/layout.tsx | 123 ++++++++++++++++++ apps/web/src/routes/admin/logs.tsx | 9 ++ apps/web/src/routes/admin/overview.tsx | 9 ++ apps/web/src/routes/admin/settings.tsx | 9 ++ .../web/src/routes/admin/users-management.tsx | 9 ++ apps/web/src/routes/events/$eventId/index.tsx | 11 ++ apps/web/src/routes/index.tsx | 4 +- 10 files changed, 186 insertions(+), 8 deletions(-) rename apps/web/src/routes/_protected/{dashboard.tsx => portal.tsx} (75%) create mode 100644 apps/web/src/routes/admin/events-management.tsx create mode 100644 apps/web/src/routes/admin/layout.tsx create mode 100644 apps/web/src/routes/admin/logs.tsx create mode 100644 apps/web/src/routes/admin/overview.tsx create mode 100644 apps/web/src/routes/admin/settings.tsx create mode 100644 apps/web/src/routes/admin/users-management.tsx create mode 100644 apps/web/src/routes/events/$eventId/index.tsx diff --git a/apps/web/src/routes/_protected/layout.tsx b/apps/web/src/routes/_protected/layout.tsx index 3bf4e9d4..70509fd6 100644 --- a/apps/web/src/routes/_protected/layout.tsx +++ b/apps/web/src/routes/_protected/layout.tsx @@ -61,10 +61,10 @@ function RouteComponent() { } - active={pathname.startsWith("/dashboard")} + active={pathname.startsWith("/portal")} /> - {/* {data.user.role === "admin" ? : } */} -

Dashboard

+

Event Portal

diff --git a/apps/web/src/routes/admin/events-management.tsx b/apps/web/src/routes/admin/events-management.tsx new file mode 100644 index 00000000..54c5815d --- /dev/null +++ b/apps/web/src/routes/admin/events-management.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute("/admin/events-management")({ + component: RouteComponent, +}); + +function RouteComponent() { + return
Hello "/admin/events-management"!
; +} diff --git a/apps/web/src/routes/admin/layout.tsx b/apps/web/src/routes/admin/layout.tsx new file mode 100644 index 00000000..087e6a61 --- /dev/null +++ b/apps/web/src/routes/admin/layout.tsx @@ -0,0 +1,123 @@ +import { AppShell } from "@/components/AppShell/AppShell"; +import { NavLink } from "@/components/AppShell/NavLink"; +import { + createFileRoute, + Outlet, + redirect, + useLocation, + useRouter, +} from "@tanstack/react-router"; +import TablerDashboard from "~icons/tabler/dashboard"; +import TablerCalendarSearch from "~icons/tabler/calendar-search"; +import TablerUsers from "~icons/tabler/users"; +import TablerListDetails from "~icons/tabler/list-details"; +import TablerSettings2 from "~icons/tabler/settings-2"; +import { Button } from "@/components/ui/Button"; + +export const Route = createFileRoute("/admin")({ + beforeLoad: async ({ context }) => { + const { user, error } = await context.userQuery.promise; + + if (!user && !user) { + console.log("You aren't authenticated, redirecting to login."); + throw redirect({ + to: "/", + search: { redirect: location.pathname }, + }); + } + + if (error) { + // TODO: Display a friendly error to the user? + console.error("Auth error in beforeLoad in layout.tsx:", error); + console.log( + "authentication error occurred while accessing protected page, redirecting to login.", + ); + throw redirect({ + to: "/", + search: { redirect: location.pathname }, + }); + } + + // If not superuser + if (user.role !== "superuser") { + console.log("You aren't a superuser, redirecting to event portal."); + throw redirect({ + to: "/portal", + }); + } + + if (location.pathname === "/admin") { + throw redirect({ to: "/admin/overview" }); + } + }, + component: RouteComponent, +}); + +function RouteComponent() { + const pathname = useLocation({ select: (loc) => loc.pathname }); + const router = useRouter(); + + return ( + + +
+

Admin Portal

+ +
+ + +
+
+
+ + + } + active={pathname.startsWith("/admin/overview")} + /> + + } + active={pathname.startsWith("/admin/events-management")} + /> + + } + active={pathname.startsWith("/admin/users-management")} + /> + + } + active={pathname.startsWith("/admin/logs")} + /> + + } + active={pathname.startsWith("/admin/settings")} + /> + + + + + +
+ ); +} diff --git a/apps/web/src/routes/admin/logs.tsx b/apps/web/src/routes/admin/logs.tsx new file mode 100644 index 00000000..24c8d1c0 --- /dev/null +++ b/apps/web/src/routes/admin/logs.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute("/admin/logs")({ + component: RouteComponent, +}); + +function RouteComponent() { + return
Hello "/admin/logs"!
; +} diff --git a/apps/web/src/routes/admin/overview.tsx b/apps/web/src/routes/admin/overview.tsx new file mode 100644 index 00000000..a1254180 --- /dev/null +++ b/apps/web/src/routes/admin/overview.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute("/admin/overview")({ + component: RouteComponent, +}); + +function RouteComponent() { + return
Hello "/admin/overview"!
; +} diff --git a/apps/web/src/routes/admin/settings.tsx b/apps/web/src/routes/admin/settings.tsx new file mode 100644 index 00000000..2ec962bb --- /dev/null +++ b/apps/web/src/routes/admin/settings.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute("/admin/settings")({ + component: RouteComponent, +}); + +function RouteComponent() { + return
Hello "/admin/settings"!
; +} diff --git a/apps/web/src/routes/admin/users-management.tsx b/apps/web/src/routes/admin/users-management.tsx new file mode 100644 index 00000000..cefda7e9 --- /dev/null +++ b/apps/web/src/routes/admin/users-management.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute("/admin/users-management")({ + component: RouteComponent, +}); + +function RouteComponent() { + return
Hello "/admin/users-management"!
; +} diff --git a/apps/web/src/routes/events/$eventId/index.tsx b/apps/web/src/routes/events/$eventId/index.tsx new file mode 100644 index 00000000..70fad662 --- /dev/null +++ b/apps/web/src/routes/events/$eventId/index.tsx @@ -0,0 +1,11 @@ +import { createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute("/events/$eventId/")({ + component: RouteComponent, +}); + +function RouteComponent() { + const { eventId } = Route.useParams(); + + return
This is the info page for {eventId}
; +} diff --git a/apps/web/src/routes/index.tsx b/apps/web/src/routes/index.tsx index 8ed251dd..a2fda4cd 100644 --- a/apps/web/src/routes/index.tsx +++ b/apps/web/src/routes/index.tsx @@ -12,9 +12,9 @@ export const Route = createFileRoute("/")({ console.log("Loaded user in beforeLoad:", user); if (user) { - console.log("User is already authenticated, redirecting to dashboard."); + console.log("User is already authenticated, redirecting to portal."); throw redirect({ - to: "/dashboard", + to: "/portal", }); } }, From 0589e02bd58dfe48eda9af81a004117b914baa58 Mon Sep 17 00:00:00 2001 From: Alexander Wang <98280966+AlexanderWangY@users.noreply.github.com> Date: Fri, 11 Jul 2025 17:13:44 -0500 Subject: [PATCH 26/52] Granulate CI/CD and only run on necessary files (#54) * fix: add dorny paths filter to only run on changed files * fix: added domain specific github actions --- .github/workflows/api/lint-api.yml | 27 ++++++++++++++ .github/workflows/api/sqlc_ci.yml | 37 +++++++++++++++++++ .../discord-bot/lint-discord-bot.yml | 15 ++++++++ .github/workflows/{ => docs}/docs.yml | 6 ++- .github/workflows/sqlc_ci.yml | 33 ----------------- .../{quality.yml => web/lint-web.yml} | 33 ++--------------- .../workflows/{web.yml => web/test-web.yml} | 9 ++--- 7 files changed, 90 insertions(+), 70 deletions(-) create mode 100644 .github/workflows/api/lint-api.yml create mode 100644 .github/workflows/api/sqlc_ci.yml create mode 100644 .github/workflows/discord-bot/lint-discord-bot.yml rename .github/workflows/{ => docs}/docs.yml (90%) delete mode 100644 .github/workflows/sqlc_ci.yml rename .github/workflows/{quality.yml => web/lint-web.yml} (52%) rename .github/workflows/{web.yml => web/test-web.yml} (92%) diff --git a/.github/workflows/api/lint-api.yml b/.github/workflows/api/lint-api.yml new file mode 100644 index 00000000..0261579d --- /dev/null +++ b/.github/workflows/api/lint-api.yml @@ -0,0 +1,27 @@ +name: API Lint + +on: + pull_request: + branches: [master] + paths: + - 'apps/api/**' + +jobs: + api-lint: + name: API Lint + runs-on: ubuntu-latest + defaults: + run: + working-directory: apps/api + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: stable + - name: Go tidy + run: go mod tidy + - name: golangci-lint + uses: golangci/golangci-lint-action@v8 + with: + version: v2.1.6 + working-directory: apps/api diff --git a/.github/workflows/api/sqlc_ci.yml b/.github/workflows/api/sqlc_ci.yml new file mode 100644 index 00000000..9f55463b --- /dev/null +++ b/.github/workflows/api/sqlc_ci.yml @@ -0,0 +1,37 @@ +name: sqlc +on: + push: + paths: + - 'apps/api/**' + +jobs: + diff: + runs-on: ubuntu-latest + defaults: + run: + working-directory: apps/api + steps: + - uses: actions/checkout@v4 + - uses: sqlc-dev/setup-sqlc@v3 + with: + sqlc-version: '1.29.0' + - run: sqlc diff + + vet: + runs-on: ubuntu-latest + defaults: + run: + working-directory: apps/api + steps: + - uses: actions/checkout@v4 + - uses: sqlc-dev/setup-sqlc@v3 + with: + sqlc-version: '1.29.0' + # Start a PostgreSQL server + - uses: sqlc-dev/action-setup-postgres@master + with: + postgres-version: "17" + id: postgres + - run: sqlc vet + env: + POSTGRESQL_SERVER_URI: ${{ steps.postgres.outputs.connection-uri }}?sslmode=disable diff --git a/.github/workflows/discord-bot/lint-discord-bot.yml b/.github/workflows/discord-bot/lint-discord-bot.yml new file mode 100644 index 00000000..d27e5ea4 --- /dev/null +++ b/.github/workflows/discord-bot/lint-discord-bot.yml @@ -0,0 +1,15 @@ +name: Discord Bot Lint + +on: + pull_request: + branches: [master] + paths: + - 'apps/discord-bot/**' + +jobs: + discord-bot-checks: + name: Discord Bot Lint & Format + runs-on: ubuntu-latest + steps: + - name: Placeholder for Discord bot lint + format + run: echo "Discord bot linting and formatting will be implemented here" diff --git a/.github/workflows/docs.yml b/.github/workflows/docs/docs.yml similarity index 90% rename from .github/workflows/docs.yml rename to .github/workflows/docs/docs.yml index 73f3bf5f..e49399ae 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs/docs.yml @@ -1,10 +1,14 @@ -name: ci +name: docs on: push: branches: - master + paths: + - 'apps/docs/**' # Only trigger if docs files changed + permissions: contents: write + jobs: deploy: runs-on: ubuntu-latest diff --git a/.github/workflows/sqlc_ci.yml b/.github/workflows/sqlc_ci.yml deleted file mode 100644 index 60d404e0..00000000 --- a/.github/workflows/sqlc_ci.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: sqlc -on: [push] -jobs: - diff: - runs-on: ubuntu-latest - defaults: - run: - working-directory: apps/api - steps: - - uses: actions/checkout@v4 - - uses: sqlc-dev/setup-sqlc@v3 - with: - sqlc-version: '1.29.0' - - run: sqlc diff - - vet: - runs-on: ubuntu-latest - defaults: - run: - working-directory: apps/api - steps: - - uses: actions/checkout@v4 - - uses: sqlc-dev/setup-sqlc@v3 - with: - sqlc-version: '1.29.0' - # Start a PostgreSQL server - - uses: sqlc-dev/action-setup-postgres@master - with: - postgres-version: "17" - id: postgres - - run: sqlc vet - env: - POSTGRESQL_SERVER_URI: ${{ steps.postgres.outputs.connection-uri }}?sslmode=disable diff --git a/.github/workflows/quality.yml b/.github/workflows/web/lint-web.yml similarity index 52% rename from .github/workflows/quality.yml rename to .github/workflows/web/lint-web.yml index ac7724d0..4b8b012a 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/web/lint-web.yml @@ -1,8 +1,10 @@ -name: quality-control +name: Web Lint on: pull_request: branches: [master] + paths: + - 'apps/web/**' jobs: web-checks: @@ -11,7 +13,6 @@ jobs: defaults: run: working-directory: apps/web - steps: - name: Checkout code uses: actions/checkout@v4 @@ -22,7 +23,6 @@ jobs: version: 10 run_install: false - - name: Install Node.js uses: actions/setup-node@v4 with: @@ -38,30 +38,3 @@ jobs: - name: Run Prettier run: pnpm prettier . --check - - - api-lint: - name: API Lint - runs-on: ubuntu-latest - defaults: - run: - working-directory: apps/api - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 - with: - go-version: stable - - name: Go tidy - run: go mod tidy - - name: golangci-lint - uses: golangci/golangci-lint-action@v8 - with: - version: v2.1.6 - working-directory: apps/api - - discord-bot-checks: - name: Discord bot Lint & Format - runs-on: ubuntu-latest - steps: - - name: Placeholder for Discord bot lint + format - run: echo "Discord bot linting and formatting will be implemented here" diff --git a/.github/workflows/web.yml b/.github/workflows/web/test-web.yml similarity index 92% rename from .github/workflows/web.yml rename to .github/workflows/web/test-web.yml index 4b2bcf0a..b58bc2c7 100644 --- a/.github/workflows/web.yml +++ b/.github/workflows/web/test-web.yml @@ -1,12 +1,11 @@ -name: web-tests +name: Web Tests on: - push: - branches: - - master pull_request: branches: - master + paths: + - 'apps/web/**' jobs: unit: @@ -26,7 +25,6 @@ jobs: version: 10 run_install: false - - name: Install Node.js uses: actions/setup-node@v4 with: @@ -42,4 +40,3 @@ jobs: - name: Run tests run: pnpm test - From 81864929b519c3ec369da41dafbdf1afb546cca1 Mon Sep 17 00:00:00 2001 From: Alexander Wang <98280966+AlexanderWangY@users.noreply.github.com> Date: Fri, 11 Jul 2025 17:36:41 -0500 Subject: [PATCH 27/52] feat: Dockerfile and workflow for building and pushing to GHCR (#55) --- .github/workflows/api/build-and-push.yml | 35 ++++++++++++++++++++++++ apps/api/.dockerignore | 3 ++ apps/api/Dockerfile | 33 ++++++++++++++++++++++ 3 files changed, 71 insertions(+) create mode 100644 .github/workflows/api/build-and-push.yml create mode 100644 apps/api/.dockerignore create mode 100644 apps/api/Dockerfile diff --git a/.github/workflows/api/build-and-push.yml b/.github/workflows/api/build-and-push.yml new file mode 100644 index 00000000..e81335ff --- /dev/null +++ b/.github/workflows/api/build-and-push.yml @@ -0,0 +1,35 @@ +name: Build and Push Docker Image to GHCR + +on: + push: + branches: + - main + paths: + - 'apps/api/**' + +permissions: + contents: read + packages: write + +jobs: + build-and-push: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build Docker image + run: | + docker build -t ghcr.io/${{ github.repository_owner }}/core-api:latest ./apps/api + + - name: Push Docker image + run: | + docker push ghcr.io/${{ github.repository_owner }}/core-api:latest diff --git a/apps/api/.dockerignore b/apps/api/.dockerignore new file mode 100644 index 00000000..6cb22def --- /dev/null +++ b/apps/api/.dockerignore @@ -0,0 +1,3 @@ +tmp/ + +Dockerignore.dev diff --git a/apps/api/Dockerfile b/apps/api/Dockerfile new file mode 100644 index 00000000..a386f16d --- /dev/null +++ b/apps/api/Dockerfile @@ -0,0 +1,33 @@ + +### STAGE 1: Build the Go binary ### +FROM golang:1.24-alpine AS builder + +# Install necessary build tools +RUN apk add --no-cache build-base ca-certificates + +# Set working directory +WORKDIR /app + +# Cache dependencies first +COPY go.mod go.sum ./ +RUN go mod download + +# Copy source +COPY . . + +# Build binary with optimizations +RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o server ./cmd/api + +### STAGE 2: Minimal runtime container ### +FROM alpine:latest + +# Install CA certs for HTTPS support +RUN apk --no-cache add ca-certificates + +WORKDIR /app + +# Copy the binary from the builder +COPY --from=builder /app/server . + +# Run the binary +CMD ["./server"] From f84579a970e1c61e5581a43ef8d3c40c94676371 Mon Sep 17 00:00:00 2001 From: h1divp <71522316+h1divp@users.noreply.github.com> Date: Fri, 11 Jul 2025 19:39:03 -0400 Subject: [PATCH 28/52] fix: got rid of extra params --- apps/api/internal/db/queries/events.sql | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/apps/api/internal/db/queries/events.sql b/apps/api/internal/db/queries/events.sql index b8f72cf6..6fbaf293 100644 --- a/apps/api/internal/db/queries/events.sql +++ b/apps/api/internal/db/queries/events.sql @@ -1,16 +1,12 @@ -- name: CreateEvent :one INSERT INTO events ( - id, name, description, - location, location_url, max_attendees, - application_open, application_close, rsvp_deadline, decision_release, + name, + application_open, application_close, start_time, end_time, - website_url ) VALUES ( - $1, $2, $3, - $4, $5, $6, - $7, $8, $9, $10, - $11, $12, - $13 + $1, + $2, $3, + $4, $5 ) RETURNING *; From 2e216971872878e2dda543d78963f0921b7a9d4e Mon Sep 17 00:00:00 2001 From: h1divp <71522316+h1divp@users.noreply.github.com> Date: Fri, 11 Jul 2025 19:43:20 -0400 Subject: [PATCH 29/52] fix: syntax --- apps/api/internal/db/queries/events.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/api/internal/db/queries/events.sql b/apps/api/internal/db/queries/events.sql index 6fbaf293..7636e5fa 100644 --- a/apps/api/internal/db/queries/events.sql +++ b/apps/api/internal/db/queries/events.sql @@ -2,7 +2,7 @@ INSERT INTO events ( name, application_open, application_close, - start_time, end_time, + start_time, end_time ) VALUES ( $1, $2, $3, From a642d148a7a00430dc7b073499d8445888c8520f Mon Sep 17 00:00:00 2001 From: h1divp <71522316+h1divp@users.noreply.github.com> Date: Fri, 11 Jul 2025 19:43:42 -0400 Subject: [PATCH 30/52] feat: application queries --- apps/api/internal/db/queries/applications.sql | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 apps/api/internal/db/queries/applications.sql diff --git a/apps/api/internal/db/queries/applications.sql b/apps/api/internal/db/queries/applications.sql new file mode 100644 index 00000000..2e553de7 --- /dev/null +++ b/apps/api/internal/db/queries/applications.sql @@ -0,0 +1,24 @@ +-- name: CreateApplication :one +INSERT INTO applications ( + user_id, event_id +) VALUES ( + $1, $2 +) +RETURNING *; + +-- name: GetApplicationByUserAndEventID :one +SELECT * FROM applications +WHERE user_id = $1 AND event_id = $2; + +-- name: UpdateApplication :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, + resume_url = CASE WHEN @resume_url_do_update::boolean THEN @resume_url ELSE resume_url END +WHERE + user_id = @user_id AND event_id = @event_id; + +-- name: DeleteApplication :exec +DELETE FROM applications +WHERE user_id = $1 AND event_id = $2; From 4a15083116826c9c96ca9e2e0b530d96a3a313f8 Mon Sep 17 00:00:00 2001 From: h1divp <71522316+h1divp@users.noreply.github.com> Date: Fri, 11 Jul 2025 19:44:06 -0400 Subject: [PATCH 31/52] chore: sqlc generate --- apps/api/internal/db/sqlc/applications.sql.go | 118 ++++++++++++++++++ apps/api/internal/db/sqlc/events.sql.go | 42 ++----- apps/api/internal/db/sqlc/querier.go | 4 + 3 files changed, 133 insertions(+), 31 deletions(-) create mode 100644 apps/api/internal/db/sqlc/applications.sql.go diff --git a/apps/api/internal/db/sqlc/applications.sql.go b/apps/api/internal/db/sqlc/applications.sql.go new file mode 100644 index 00000000..f8d9a23b --- /dev/null +++ b/apps/api/internal/db/sqlc/applications.sql.go @@ -0,0 +1,118 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 +// source: applications.sql + +package sqlc + +import ( + "context" + + "github.com/google/uuid" +) + +const createApplication = `-- name: CreateApplication :one +INSERT INTO applications ( + user_id, event_id +) VALUES ( + $1, $2 +) +RETURNING user_id, event_id, status, application, resume_url, created_at, saved_at, updated_at +` + +type CreateApplicationParams struct { + UserID uuid.UUID `json:"user_id"` + EventID uuid.UUID `json:"event_id"` +} + +func (q *Queries) CreateApplication(ctx context.Context, arg CreateApplicationParams) (Application, error) { + row := q.db.QueryRow(ctx, createApplication, arg.UserID, arg.EventID) + var i Application + err := row.Scan( + &i.UserID, + &i.EventID, + &i.Status, + &i.Application, + &i.ResumeUrl, + &i.CreatedAt, + &i.SavedAt, + &i.UpdatedAt, + ) + return i, err +} + +const deleteApplication = `-- name: DeleteApplication :exec +DELETE FROM applications +WHERE user_id = $1 AND event_id = $2 +` + +type DeleteApplicationParams struct { + UserID uuid.UUID `json:"user_id"` + EventID uuid.UUID `json:"event_id"` +} + +func (q *Queries) DeleteApplication(ctx context.Context, arg DeleteApplicationParams) error { + _, err := q.db.Exec(ctx, deleteApplication, arg.UserID, arg.EventID) + return err +} + +const getApplicationByUserAndEventID = `-- name: GetApplicationByUserAndEventID :one +SELECT user_id, event_id, status, application, resume_url, created_at, saved_at, updated_at FROM applications +WHERE user_id = $1 AND event_id = $2 +` + +type GetApplicationByUserAndEventIDParams struct { + UserID uuid.UUID `json:"user_id"` + EventID uuid.UUID `json:"event_id"` +} + +func (q *Queries) GetApplicationByUserAndEventID(ctx context.Context, arg GetApplicationByUserAndEventIDParams) (Application, error) { + row := q.db.QueryRow(ctx, getApplicationByUserAndEventID, arg.UserID, arg.EventID) + var i Application + err := row.Scan( + &i.UserID, + &i.EventID, + &i.Status, + &i.Application, + &i.ResumeUrl, + &i.CreatedAt, + &i.SavedAt, + &i.UpdatedAt, + ) + return i, err +} + +const updateApplication = `-- name: UpdateApplication :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, + resume_url = CASE WHEN $5::boolean THEN $6 ELSE resume_url END +WHERE + user_id = $7 AND event_id = $8 +` + +type UpdateApplicationParams struct { + StatusDoUpdate bool `json:"status_do_update"` + Status ApplicationStatus `json:"status"` + ApplicationDoUpdate bool `json:"application_do_update"` + Application []byte `json:"application"` + ResumeUrlDoUpdate bool `json:"resume_url_do_update"` + ResumeUrl *string `json:"resume_url"` + UserID uuid.UUID `json:"user_id"` + EventID uuid.UUID `json:"event_id"` +} + +func (q *Queries) UpdateApplication(ctx context.Context, arg UpdateApplicationParams) error { + _, err := q.db.Exec(ctx, updateApplication, + arg.StatusDoUpdate, + arg.Status, + arg.ApplicationDoUpdate, + arg.Application, + arg.ResumeUrlDoUpdate, + arg.ResumeUrl, + arg.UserID, + arg.EventID, + ) + return err +} diff --git a/apps/api/internal/db/sqlc/events.sql.go b/apps/api/internal/db/sqlc/events.sql.go index e10d6cd9..72841755 100644 --- a/apps/api/internal/db/sqlc/events.sql.go +++ b/apps/api/internal/db/sqlc/events.sql.go @@ -14,52 +14,32 @@ import ( const createEvent = `-- name: CreateEvent :one INSERT INTO events ( - id, name, description, - location, location_url, max_attendees, - application_open, application_close, rsvp_deadline, decision_release, - start_time, end_time, - website_url + name, + application_open, application_close, + start_time, end_time ) VALUES ( - $1, $2, $3, - $4, $5, $6, - $7, $8, $9, $10, - $11, $12, - $13 + $1, + $2, $3, + $4, $5 ) RETURNING id, name, description, location, location_url, max_attendees, application_open, application_close, rsvp_deadline, decision_release, start_time, end_time, website_url, is_published, created_at, updated_at ` type CreateEventParams struct { - ID uuid.UUID `json:"id"` - Name string `json:"name"` - Description *string `json:"description"` - Location *string `json:"location"` - LocationUrl *string `json:"location_url"` - MaxAttendees *int32 `json:"max_attendees"` - ApplicationOpen time.Time `json:"application_open"` - ApplicationClose time.Time `json:"application_close"` - RsvpDeadline *time.Time `json:"rsvp_deadline"` - DecisionRelease *time.Time `json:"decision_release"` - StartTime time.Time `json:"start_time"` - EndTime time.Time `json:"end_time"` - WebsiteUrl *string `json:"website_url"` + Name string `json:"name"` + ApplicationOpen time.Time `json:"application_open"` + ApplicationClose time.Time `json:"application_close"` + StartTime time.Time `json:"start_time"` + EndTime time.Time `json:"end_time"` } func (q *Queries) CreateEvent(ctx context.Context, arg CreateEventParams) (Event, error) { row := q.db.QueryRow(ctx, createEvent, - arg.ID, arg.Name, - arg.Description, - arg.Location, - arg.LocationUrl, - arg.MaxAttendees, arg.ApplicationOpen, arg.ApplicationClose, - arg.RsvpDeadline, - arg.DecisionRelease, arg.StartTime, arg.EndTime, - arg.WebsiteUrl, ) var i Event err := row.Scan( diff --git a/apps/api/internal/db/sqlc/querier.go b/apps/api/internal/db/sqlc/querier.go index b649a3a3..1df6a562 100644 --- a/apps/api/internal/db/sqlc/querier.go +++ b/apps/api/internal/db/sqlc/querier.go @@ -16,14 +16,17 @@ type Querier interface { // Returns the newly created email record. AddEmail(ctx context.Context, arg AddEmailParams) (EventInterestSubmission, error) CreateAccount(ctx context.Context, arg CreateAccountParams) (AuthAccount, error) + CreateApplication(ctx context.Context, arg CreateApplicationParams) (Application, error) CreateEvent(ctx context.Context, arg CreateEventParams) (Event, error) CreateSession(ctx context.Context, arg CreateSessionParams) (AuthSession, error) CreateUser(ctx context.Context, arg CreateUserParams) (AuthUser, error) DeleteAccount(ctx context.Context, arg DeleteAccountParams) error + DeleteApplication(ctx context.Context, arg DeleteApplicationParams) error DeleteEvent(ctx context.Context, id uuid.UUID) error DeleteExpiredSession(ctx context.Context) error DeleteUser(ctx context.Context, id uuid.UUID) error GetActiveSessionUserInfo(ctx context.Context, id uuid.UUID) (GetActiveSessionUserInfoRow, error) + GetApplicationByUserAndEventID(ctx context.Context, arg GetApplicationByUserAndEventIDParams) (Application, error) GetByProviderAndAccountID(ctx context.Context, arg GetByProviderAndAccountIDParams) (AuthAccount, error) GetByUserID(ctx context.Context, userID uuid.UUID) ([]AuthAccount, error) GetEventByID(ctx context.Context, id uuid.UUID) (Event, error) @@ -34,6 +37,7 @@ type Querier interface { GetUserByID(ctx context.Context, id uuid.UUID) (AuthUser, error) InvalidateSessionByID(ctx context.Context, id uuid.UUID) error TouchSession(ctx context.Context, arg TouchSessionParams) error + UpdateApplication(ctx context.Context, arg UpdateApplicationParams) error UpdateEvent(ctx context.Context, arg UpdateEventParams) error UpdateSessionExpiration(ctx context.Context, arg UpdateSessionExpirationParams) error UpdateTokens(ctx context.Context, arg UpdateTokensParams) error From fc665d2afecbcd4c1dc37f621b3356d3e7f22fc7 Mon Sep 17 00:00:00 2001 From: h1divp <71522316+h1divp@users.noreply.github.com> Date: Wed, 16 Jul 2025 19:13:47 -0400 Subject: [PATCH 32/52] Fix: apps can now run --- apps/api/go.mod | 1 - docker-compose.yml | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/api/go.mod b/apps/api/go.mod index 73abb4b0..ff1187dd 100644 --- a/apps/api/go.mod +++ b/apps/api/go.mod @@ -9,7 +9,6 @@ require ( github.com/google/uuid v1.6.0 github.com/jackc/pgx/v5 v5.7.4 github.com/joho/godotenv v1.5.1 - github.com/lib/pq v1.10.9 github.com/rs/zerolog v1.34.0 ) diff --git a/docker-compose.yml b/docker-compose.yml index 292d7adb..d7bb5bc8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,6 +17,7 @@ services: dockerfile: Dockerfile.dev volumes: - ./apps/web:/app + - web_node_modules:/app/node_modules ports: - "5173:5173" environment: @@ -46,5 +47,6 @@ services: volumes: postgres_data: + web_node_modules: From 022d57f509cfcfbb14612624f1a49a58065d12d9 Mon Sep 17 00:00:00 2001 From: h1divp <71522316+h1divp@users.noreply.github.com> Date: Thu, 17 Jul 2025 15:25:04 -0400 Subject: [PATCH 33/52] docs: db testing --- apps/docs/mkdocs.yml | 1 + apps/docs/src/api/db_testing.md | 30 ++++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 apps/docs/src/api/db_testing.md diff --git a/apps/docs/mkdocs.yml b/apps/docs/mkdocs.yml index 21a8abd8..40af77ab 100644 --- a/apps/docs/mkdocs.yml +++ b/apps/docs/mkdocs.yml @@ -50,6 +50,7 @@ nav: - Overview: api/index.md - Installation: api/installation.md - OpenAPI: 'https://core.apidocumentation.com/guide/swamphacks-core-api' + - Database testing: 'api/db_testing.md' - Discord Bot: - Overview: discord-bot/index.md - Installation: discord-bot/installation.md diff --git a/apps/docs/src/api/db_testing.md b/apps/docs/src/api/db_testing.md new file mode 100644 index 00000000..6a0d5f19 --- /dev/null +++ b/apps/docs/src/api/db_testing.md @@ -0,0 +1,30 @@ +# Database testing + +## Manual + +1. Make sure the docker instance is currently running (i.e. you ran `$ docker compose up`) +!!! note + + You may have to run docker with sudo depending on your system configuration. + +1. In a seperate terminal (I suggest you try using tmux) list the current docker processes +``` bash +docker ps +``` + +1. Copy the process id for the docker container running postgres, and paste into the following command in order to spawn a shell with access to the container. +``` bash +sudo docker exec -it ef7XXXXXX07e sh +``` + +1. Connect to the postgres database using a database url. The url can be found inside `core/apps/api/.env.example`. +``` bash +psql postgres://postgres:postgres@postgres:5432/coredb +``` + +1. Check to see if database tables currently exist. You can by listing the currently created tables. +``` bash +\dt +``` +If there is nothing here, then go into `/core/apps/api` and run `make migrate`, which runs sql commands added to [migrations](https://en.wikipedia.org/wiki/Schema_migration) in `core/apps/api/internal/db/migrations`. +1. You can now test to see if rows, columns and tables are updated appropriately with psql commands. Use a [reference to psql](https://www.postgresql.org/docs/17/app-psql.html) if you need help finding commands. From c1e864814417025458b2142ba4af7a41340621df Mon Sep 17 00:00:00 2001 From: Hugo Liu <98724522+hugoliu-code@users.noreply.github.com> Date: Tue, 8 Jul 2025 16:28:40 -0700 Subject: [PATCH 34/52] TECH-107: Add mailing list (#40) * Sqlc code and goose * Added mailing list functionality * ref: change to event interest submissions * fix: move to pgconn for error handling * fix: change to satisfy linter --------- Co-authored-by: Alexander Wang --- apps/api/go.mod | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/api/go.mod b/apps/api/go.mod index ff1187dd..73abb4b0 100644 --- a/apps/api/go.mod +++ b/apps/api/go.mod @@ -9,6 +9,7 @@ require ( github.com/google/uuid v1.6.0 github.com/jackc/pgx/v5 v5.7.4 github.com/joho/godotenv v1.5.1 + github.com/lib/pq v1.10.9 github.com/rs/zerolog v1.34.0 ) From 664c72454696b4205ee79dfb5d4cfb70e83ce2c3 Mon Sep 17 00:00:00 2001 From: Alexander Wang <98280966+AlexanderWangY@users.noreply.github.com> Date: Fri, 11 Jul 2025 18:05:40 -0500 Subject: [PATCH 35/52] Fix/discord pfp (#56) * fix: parse discord pfp and save as avatar or default * fix: change default from svg to png * fix: fix non nestable workflows --- .../workflows/{api => }/build-and-push.yml | 2 +- .github/workflows/{docs => }/docs.yml | 0 .github/workflows/{api => }/lint-api.yml | 0 .../{discord-bot => }/lint-discord-bot.yml | 0 .github/workflows/{web => }/lint-web.yml | 0 .github/workflows/{api => }/sqlc_ci.yml | 0 .github/workflows/{web => }/test-web.yml | 0 apps/api/internal/oauth/discord.go | 45 ++++++++++++++++--- apps/api/internal/services/auth.go | 12 ++++- 9 files changed, 49 insertions(+), 10 deletions(-) rename .github/workflows/{api => }/build-and-push.yml (98%) rename .github/workflows/{docs => }/docs.yml (100%) rename .github/workflows/{api => }/lint-api.yml (100%) rename .github/workflows/{discord-bot => }/lint-discord-bot.yml (100%) rename .github/workflows/{web => }/lint-web.yml (100%) rename .github/workflows/{api => }/sqlc_ci.yml (100%) rename .github/workflows/{web => }/test-web.yml (100%) diff --git a/.github/workflows/api/build-and-push.yml b/.github/workflows/build-and-push.yml similarity index 98% rename from .github/workflows/api/build-and-push.yml rename to .github/workflows/build-and-push.yml index e81335ff..e488d9ea 100644 --- a/.github/workflows/api/build-and-push.yml +++ b/.github/workflows/build-and-push.yml @@ -3,7 +3,7 @@ name: Build and Push Docker Image to GHCR on: push: branches: - - main + - master paths: - 'apps/api/**' diff --git a/.github/workflows/docs/docs.yml b/.github/workflows/docs.yml similarity index 100% rename from .github/workflows/docs/docs.yml rename to .github/workflows/docs.yml diff --git a/.github/workflows/api/lint-api.yml b/.github/workflows/lint-api.yml similarity index 100% rename from .github/workflows/api/lint-api.yml rename to .github/workflows/lint-api.yml diff --git a/.github/workflows/discord-bot/lint-discord-bot.yml b/.github/workflows/lint-discord-bot.yml similarity index 100% rename from .github/workflows/discord-bot/lint-discord-bot.yml rename to .github/workflows/lint-discord-bot.yml diff --git a/.github/workflows/web/lint-web.yml b/.github/workflows/lint-web.yml similarity index 100% rename from .github/workflows/web/lint-web.yml rename to .github/workflows/lint-web.yml diff --git a/.github/workflows/api/sqlc_ci.yml b/.github/workflows/sqlc_ci.yml similarity index 100% rename from .github/workflows/api/sqlc_ci.yml rename to .github/workflows/sqlc_ci.yml diff --git a/.github/workflows/web/test-web.yml b/.github/workflows/test-web.yml similarity index 100% rename from .github/workflows/web/test-web.yml rename to .github/workflows/test-web.yml diff --git a/apps/api/internal/oauth/discord.go b/apps/api/internal/oauth/discord.go index 6c5336b7..4d029bc2 100644 --- a/apps/api/internal/oauth/discord.go +++ b/apps/api/internal/oauth/discord.go @@ -20,11 +20,16 @@ var ( ) type DiscordUser struct { - ID string `json:"id"` - Username string `json:"username"` - Avatar string `json:"avatar"` - Discriminator string `json:"discriminator"` - Email string `json:"email"` + ID string `json:"id"` + Username string `json:"username"` + Avatar *string `json:"avatar"` + Discriminator string `json:"discriminator"` + Email string `json:"email"` +} + +type DiscordUserWithAvatarURL struct { + DiscordUser + AvatarURL *string } // Note: expiresIn is in seconds @@ -71,7 +76,7 @@ func ExchangeDiscordCode(ctx context.Context, client *http.Client, oauthCfg *con } -func GetDiscordUserInfo(ctx context.Context, client *http.Client, accessToken string) (*DiscordUser, error) { +func GetDiscordUserInfo(ctx context.Context, client *http.Client, accessToken string) (*DiscordUserWithAvatarURL, error) { req, err := http.NewRequestWithContext(ctx, "GET", "https://discord.com/api/users/@me", nil) if err != nil { return nil, err @@ -96,7 +101,33 @@ func GetDiscordUserInfo(ctx context.Context, client *http.Client, accessToken st return nil, err } - return &user, nil + // Parse real avatar URL or make nil + userWithAvatar := DiscordUserWithAvatarURL{ + DiscordUser: user, + AvatarURL: user.AvatarURL(), + } + + return &userWithAvatar, nil +} + +func (u *DiscordUser) AvatarURL() *string { + // Only proceed if Avatar is non-nil *and* not the empty string + if u.Avatar != nil && *u.Avatar != "" { + hash := *u.Avatar + + ext := "png" + if strings.HasPrefix(hash, "a_") { + ext = "gif" + } + + url := fmt.Sprintf( + "https://cdn.discordapp.com/avatars/%s/%s.%s", + u.ID, hash, ext, + ) + return &url + } + + return nil } // TODO: Refactor more cleanly diff --git a/apps/api/internal/services/auth.go b/apps/api/internal/services/auth.go index f567f4a0..2c0a901b 100644 --- a/apps/api/internal/services/auth.go +++ b/apps/api/internal/services/auth.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "net/http" + "net/url" "time" "github.com/google/uuid" @@ -109,7 +110,7 @@ func (s *AuthService) authenticateWithDiscord(ctx context.Context, code string, return s.createSessionForExistingUser(ctx, account.UserID, ipAddress, userAgent) } -func (s *AuthService) registerNewDiscordUser(ctx context.Context, userInfo *oauth.DiscordUser, oauthResp *oauth.DiscordExchangeResponse, ipAddress, userAgent *string) (*sqlc.AuthSession, error) { +func (s *AuthService) registerNewDiscordUser(ctx context.Context, userInfo *oauth.DiscordUserWithAvatarURL, oauthResp *oauth.DiscordExchangeResponse, ipAddress, userAgent *string) (*sqlc.AuthSession, error) { var session *sqlc.AuthSession err := s.txm.WithTx(ctx, func(tx pgx.Tx) error { @@ -117,10 +118,17 @@ func (s *AuthService) registerNewDiscordUser(ctx context.Context, userInfo *oaut txAccountRepo := s.accountRepo.NewTx(tx) txSessionRepo := s.sessionRepo.NewTx(tx) + // Default avatar if no discord avatar + avatar := userInfo.AvatarURL + if avatar == nil { + custom := fmt.Sprintf("https://api.dicebear.com/9.x/initials/png?seed=%s", url.QueryEscape(userInfo.Username)) + avatar = &custom + } + user, err := txUserRepo.Create(ctx, sqlc.CreateUserParams{ Name: userInfo.Username, Email: &userInfo.Email, - Image: &userInfo.Avatar, + Image: avatar, }) if err != nil { return err From 99ea7bcac249b94eb6ddaa2f5278ff334888778f Mon Sep 17 00:00:00 2001 From: Alexander Wang <98280966+AlexanderWangY@users.noreply.github.com> Date: Fri, 11 Jul 2025 18:16:14 -0500 Subject: [PATCH 36/52] fix: allow go sum for build (#57) --- ...{build-and-push.yml => build-push-api.yml} | 0 .gitignore | 3 - apps/api/Dockerfile | 11 ---- apps/api/go.sum | 56 +++++++++++++++++++ 4 files changed, 56 insertions(+), 14 deletions(-) rename .github/workflows/{build-and-push.yml => build-push-api.yml} (100%) create mode 100644 apps/api/go.sum diff --git a/.github/workflows/build-and-push.yml b/.github/workflows/build-push-api.yml similarity index 100% rename from .github/workflows/build-and-push.yml rename to .github/workflows/build-push-api.yml diff --git a/.gitignore b/.gitignore index e79d68e1..6e7eef84 100644 --- a/.gitignore +++ b/.gitignore @@ -34,9 +34,6 @@ bin/ coverage/ *.prof -# Dependency/package management -go.sum - # Anything Python related venv diff --git a/apps/api/Dockerfile b/apps/api/Dockerfile index a386f16d..eb9300c0 100644 --- a/apps/api/Dockerfile +++ b/apps/api/Dockerfile @@ -1,33 +1,22 @@ - -### STAGE 1: Build the Go binary ### FROM golang:1.24-alpine AS builder -# Install necessary build tools RUN apk add --no-cache build-base ca-certificates -# Set working directory WORKDIR /app -# Cache dependencies first COPY go.mod go.sum ./ RUN go mod download -# Copy source COPY . . -# Build binary with optimizations RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o server ./cmd/api -### STAGE 2: Minimal runtime container ### FROM alpine:latest -# Install CA certs for HTTPS support RUN apk --no-cache add ca-certificates WORKDIR /app -# Copy the binary from the builder COPY --from=builder /app/server . -# Run the binary CMD ["./server"] diff --git a/apps/api/go.sum b/apps/api/go.sum new file mode 100644 index 00000000..3e9e272b --- /dev/null +++ b/apps/api/go.sum @@ -0,0 +1,56 @@ +github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA= +github.com/caarlos0/env/v11 v11.3.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618= +github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= +github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= +github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.7.4 h1:9wKznZrhWa2QiHL+NjTSPP6yjl3451BX3imWDnokYlg= +github.com/jackc/pgx/v5 v5.7.4/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= +github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= +golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= +golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= +golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= +golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From 160d775e9fba510503b5b08cb4e339104b3cfe4a Mon Sep 17 00:00:00 2001 From: Alexander Wang <98280966+AlexanderWangY@users.noreply.github.com> Date: Fri, 11 Jul 2025 20:38:58 -0400 Subject: [PATCH 37/52] Stanley/basic bot structure (#59) * chore(env): setup development and production python venvs * feat: add message trigger command for bot * Create readme.md (#3) * chore: complete merge conflict and rebase * chore: rewrite readme * feat: add regex to identify potential scam/spam messages in antiSpam cog * refactor: organize and break up functions, add detailed comments, add remove role command * refactor: include direction in readme to select interpreter and add any pycache or venvs file types to gitignore * feat: create panel command and reusable buttons added * feat: update anti_spam to have new spam detection, add support cog with request features * add mentor role functions and rework ticket panel * feat: implement round-robin for pinging available mentors * feat: make threads archived, add limits to user inputs, clear selection options * feat: add mod command to grant user access to private vcs --------- Co-authored-by: Stanley Co-authored-by: Phoenix <71522316+h1divp@users.noreply.github.com> Co-authored-by: Stanley Ke --- apps/discord-bot/.gitignore | 4 +- apps/discord-bot/README.md | 11 +- apps/discord-bot/cogs/anti_spam.py | 155 +++++++--- apps/discord-bot/cogs/general.py | 181 +++++++++++- apps/discord-bot/cogs/support.py | 81 ++++++ .../components/mentor_ping_state.py | 1 + apps/discord-bot/components/support_modals.py | 272 ++++++++++++++++++ .../components/support_thread_buttons.py | 145 ++++++++++ .../components/support_vc_buttons.py | 126 ++++++++ apps/discord-bot/components/ticket_state.py | 1 + apps/discord-bot/components/ticket_view.py | 98 +++++++ apps/discord-bot/config.json | 5 + apps/discord-bot/main.py | 29 +- apps/discord-bot/utils/checks.py | 38 +++ .../utils/get_next_support_vc_name.py | 26 ++ .../discord-bot/utils/get_next_thread_name.py | 26 ++ apps/discord-bot/utils/mentor_functions.py | 54 ++++ 17 files changed, 1184 insertions(+), 69 deletions(-) create mode 100644 apps/discord-bot/cogs/support.py create mode 100644 apps/discord-bot/components/mentor_ping_state.py create mode 100644 apps/discord-bot/components/support_modals.py create mode 100644 apps/discord-bot/components/support_thread_buttons.py create mode 100644 apps/discord-bot/components/support_vc_buttons.py create mode 100644 apps/discord-bot/components/ticket_state.py create mode 100644 apps/discord-bot/components/ticket_view.py create mode 100644 apps/discord-bot/config.json create mode 100644 apps/discord-bot/utils/checks.py create mode 100644 apps/discord-bot/utils/get_next_support_vc_name.py create mode 100644 apps/discord-bot/utils/get_next_thread_name.py create mode 100644 apps/discord-bot/utils/mentor_functions.py diff --git a/apps/discord-bot/.gitignore b/apps/discord-bot/.gitignore index dc29836a..e8bdbb2c 100644 --- a/apps/discord-bot/.gitignore +++ b/apps/discord-bot/.gitignore @@ -1,3 +1,5 @@ .env /core/apps/discord-bot/.venv* -*.log \ No newline at end of file +*.log +__pycache__/ +.venv/ \ No newline at end of file diff --git a/apps/discord-bot/README.md b/apps/discord-bot/README.md index 2b02e305..9ef0b1ec 100644 --- a/apps/discord-bot/README.md +++ b/apps/discord-bot/README.md @@ -4,10 +4,10 @@ Before you begin, make sure you have: -* **Python 3.8+** installed and on your PATH -* **Git** (optional if you clone the repo) +- **Python 3.8+** installed and on your PATH +- **Git** (optional if you clone the repo) -*All commands assume you are in the `core/apps/discord-bot/` directory.* +_All commands assume you are in the `core/apps/discord-bot/` directory._ ## 1. Create a Virtual Environment @@ -15,6 +15,8 @@ Before you begin, make sure you have: python -m venv .venv ``` +Make sure to select the path to the interpreter usually located in .venv/bin/pythonX + ## 2. Activate the Environment ### macOS / Linux @@ -29,7 +31,7 @@ source .venv/bin/activate .\.venv\Scripts\Activate.ps1 ``` -*(Your prompt should now be prefixed with `(.venv)`.)* +_(Your prompt should now be prefixed with `(.venv)`.)_ ## 3. Install Dependencies @@ -72,4 +74,3 @@ Bot: SwampHackr is ready to go! > # or > .\.venv\Scripts\Activate.ps1 # (Windows) > ``` - diff --git a/apps/discord-bot/cogs/anti_spam.py b/apps/discord-bot/cogs/anti_spam.py index 11de678b..b694b79f 100644 --- a/apps/discord-bot/cogs/anti_spam.py +++ b/apps/discord-bot/cogs/anti_spam.py @@ -3,36 +3,38 @@ import re from typing import List, Pattern, Set from utils.messaging import send_channel_message, send_dm - +from collections import defaultdict, deque +import time +from discord.utils import utcnow +import datetime +from collections import Counter # Regular expressions for detecting spam patterns SPAM_PATTERNS: List[Pattern] = [ - # Pattern for detecting ticket sales - re.compile(r""" - (?i) - \bselling\b - .{0,60}? - (tickets?|passes?|spots?|seats?) - (.{0,20}?\bfor\b.{0,20}?\$?\d{2,5})? - """, re.VERBOSE), - - # Pattern for detecting subleasing advertisements - re.compile(r""" - (?i) - \b(sublease|subleasing|sublet|lease)\b - (.{0,20}?\bfor\b.{0,20}?\$?\d{2,5})? - """, re.VERBOSE), - - # Pattern for detecting "DM if interested" messages - re.compile(r""" - (?i) - dm\s+ - (me\s+)?(if\s+)?interested\b - """, re.VERBOSE), + # # Pattern for detecting ticket sales + # re.compile(r""" + # (?i) + # \bselling\b + # (.{0,20}?\bfor\b.{0,20}?\$?\d{2,5})? + # """, re.VERBOSE), + + # # Pattern for detecting subleasing advertisements + # re.compile(r""" + # (?i) + # \b(sublease|subleasing|sublet|lease)\b + # (.{0,20}?\bfor\b.{0,20}?\$?\d{2,5})? + # """, re.VERBOSE), + + # # Pattern for detecting "DM if interested" messages + # re.compile(r""" + # (?i) + # dm\s+ + # (me\s+)?(if\s+)?interested\b + # """, re.VERBOSE), + ] - class AntiSpam(commands.Cog): """A cog that detects and handles spam messages in Discord channels. @@ -49,9 +51,56 @@ def __init__(self, bot: commands.Bot) -> None: """ self.bot: commands.Bot = bot self.ignore_channels: Set[int] = set() + self.user_message_history: dict[int, deque[tuple[str, float]]] = defaultdict(deque) + self.repetition_window_sec = 30 + self.repetition_threshold = 5 + + def is_repeated_message(self, message: discord.Message) -> tuple[bool, str | None]: + """Check if a message is repeated + + Args: + message: Discord message to check + """ + user_id = message.author.id + content = message.content.strip().lower() + now = time.time() + + history = self.user_message_history[user_id] + + new_history = deque(maxlen=self.repetition_threshold) + for msg, timestamp in history: + if now - timestamp < self.repetition_window_sec: + new_history.append((msg, timestamp)) + + new_history.append((content, now)) + self.user_message_history[user_id] = new_history + + repeat_count = 0 + for msg, _ in new_history: + if msg == content: + repeat_count += 1 + + if repeat_count >= self.repetition_threshold: + return True, "Your message has been deleted for repeated content and spam." + return False, None + def is_excessive(self, message: discord.Message) -> tuple[bool, str | None]: + """Check if a message has excessive spam or mentions + + Args: + message: Discord message to check + """ + words = message.content.split() + words_count = Counter(words) + for word, count in words_count.items(): + if word.startswith("<@") and count > 3: + return True, f"Please refrain from excessively mentioning another user." + if count > 10: + return True, f"Your message has been deleted for excessive spamming {word}." + + return False, None - def is_spam(self, message: discord.Message) -> bool: + def is_spam(self, message: discord.Message) -> tuple[bool, str | None]: """Check if a message matches any spam patterns Args: @@ -60,21 +109,38 @@ def is_spam(self, message: discord.Message) -> bool: Returns: bool: True if the message is determined to be spam, False otherwise """ - content: str = message.content - for pattern in SPAM_PATTERNS: - if pattern.search(content): - return True - # TODO: Implement additional spam detection methods: + # use soft delete method (immediately halt the user and then asynchronously delete the messages) # - Check for repeated messages # - Detect excessive mentions # - Check for suspicious links # - Monitor message frequency - return False + # Check repeated messages + repeated, reason = self.is_repeated_message(message) + if repeated: + return True, reason + + # Check excessive mentions + mentions, reason = self.is_excessive(message) + if mentions: + return True, reason + + return False, None + + async def timeout_user(self, member: discord.Member, reason: str = "Spam", duration_seconds: int = 30) -> None: + try: + until = utcnow() + datetime.timedelta(seconds=duration_seconds) + await member.timeout(until, reason=reason) + except discord.Forbidden: + print("Bot does not have permission to timeout members.") + except discord.HTTPException as e: + print(f"Failed to timeout member: {e}") + except TypeError as e: + print(f"Type error when applying timeout: {e}") - async def handle_spam(self, message: discord.Message) -> None: + async def handle_spam(self, message: discord.Message, reason: str | None = None) -> None: """Handle a detected spam message This method: @@ -90,16 +156,13 @@ async def handle_spam(self, message: discord.Message) -> None: the error will be logged but not raised. """ try: + if isinstance(message.author, discord.Member): + await self.timeout_user(message.author, reason) + await message.delete() - content: str = f"{message.author.mention} Your message has been deleted for potential scam/spam." - await send_channel_message(message.channel, content, delete_after=5) - - content = ( - f"Your message was automatically deleted for potential scam/spam content:\n" - f"```{message.content}```\n" - f"If you believe this was a mistake, please contact server staff for review." - ) - await send_dm(message.author, content) + content: str = f"{message.author.mention} {reason}" + await send_channel_message(message.channel, content, delete_after=15) + except discord.Forbidden: print("Bot lacks permissions to delete messages") @@ -115,14 +178,12 @@ async def on_message(self, message: discord.Message) -> None: Args: message: The message that triggered the event """ - if message.author.bot: - return - - if message.channel.id in self.ignore_channels: + if message.author.bot or message.channel.id in self.ignore_channels: return - if self.is_spam(message): - await self.handle_spam(message) + is_spam, reason = self.is_spam(message) + if is_spam: + await self.handle_spam(message, reason) async def setup(bot: commands.Bot) -> None: diff --git a/apps/discord-bot/cogs/general.py b/apps/discord-bot/cogs/general.py index 9fdf5112..30d112ac 100644 --- a/apps/discord-bot/cogs/general.py +++ b/apps/discord-bot/cogs/general.py @@ -1,7 +1,9 @@ from discord.ext import commands from discord import app_commands import discord -from typing import Optional, Literal +from typing import Literal +from utils.checks import is_mod_slash +from utils.mentor_functions import set_all_mentors_available class General(commands.Cog): @@ -12,7 +14,6 @@ class General(commands.Cog): - Role management - Fun commands """ - def __init__(self, bot: commands.Bot) -> None: """Initialize the General cog @@ -21,6 +22,10 @@ def __init__(self, bot: commands.Bot) -> None: """ self.bot: commands.Bot = bot + def get_role(self, guild: discord.Guild, role_name: str) -> discord.Role: + """Helper to get a role by name from a guild.""" + return discord.utils.get(guild.roles, name=role_name) + @commands.command() async def test(self, ctx: commands.Context) -> None: """Send a test message @@ -30,6 +35,77 @@ async def test(self, ctx: commands.Context) -> None: """ await ctx.send("Testing") + @app_commands.command(name="delete", description="Delete X amount of messages based on the number you provide") + @app_commands.describe( + amount="The amount of messages to delete" + ) + @is_mod_slash() + async def delete( + self, + interaction: discord.Interaction, + amount: int + ) -> None: + """Delete X amount of messages based on the number you provide + + Args: + interaction: The interaction that triggered this command + amount: The amount of messages to delete + """ + await interaction.response.defer(ephemeral=True) + deleted = await interaction.channel.purge(limit=amount) + await interaction.followup.send( + f"Deleted {len(deleted)} messages.", + ephemeral=True + ) + + @app_commands.command(name="delete_all_threads", description="Delete all threads in a specified channel") + @is_mod_slash() + async def delete_all_threads(self, interaction: discord.Interaction, channel: discord.TextChannel, delete_archived: bool = False) -> None: + """Delete all threads in a specified channel + + Args: + interaction: The interaction that triggered this command + """ + guild = interaction.guild + if channel not in guild.text_channels: + await interaction.response.send_message("Error: Could not find the specified channel.", ephemeral=True) + return + + for thread in channel.threads: + # this only iterates over active threads so archived threads will not be deleted + await thread.delete() + + if delete_archived: + # delete archived public or private threads + async for thread in channel.archived_threads(private=False): + await thread.delete() + async for thread in channel.archived_threads(private=True): + await thread.delete() + + await interaction.response.send_message( + f"All threads in {channel.mention} {'including archived ones ' if delete_archived else ''}have been deleted.", + ephemeral=True + ) + + @app_commands.command(name="delete_all_vcs", description="Delete all voice channels in a specified category") + @is_mod_slash() + async def delete_all_vcs(self, interaction: discord.Interaction, category: discord.CategoryChannel) -> None: + """Delete all voice channels in a specified category + + Args: + interaction: The interaction that triggered this command + category: The category from which to delete all voice channels + """ + guild = interaction.guild + if category not in guild.categories: + await interaction.response.send_message("Error: Could not find the specified category.", ephemeral=True) + return + + for channel in category.voice_channels: + await channel.delete() + + await interaction.response.send_message("All voice channels in the specified category have been deleted.", ephemeral=True) + @app_commands.command( name="role", description="Assign or remove a role from yourself" @@ -38,6 +114,7 @@ async def test(self, ctx: commands.Context) -> None: action="Whether to assign or remove the role", role="The role to assign or remove" ) + @is_mod_slash() async def manage_role( self, interaction: discord.Interaction, @@ -58,18 +135,9 @@ async def manage_role( 2. Assign or remove the role if conditions are met 3. Send appropriate feedback messages """ - staff_role = discord.utils.get(interaction.guild.roles, name="Staff") - - if not staff_role or staff_role not in interaction.user.roles: - await interaction.response.send_message( - "You don't have permission to use this command.", - ephemeral=True - ) - return - + # fetch the member to give the role to member = await interaction.guild.fetch_member(member.id) - has_role = role in member.roles if action == "assign": @@ -126,8 +194,93 @@ async def manage_role( "Invalid action. Please use 'assign' or 'remove'.", ephemeral=True ) - - + + @is_mod_slash() + @app_commands.command(name="set_available_mentors", description="Set available mentors in the server") + async def set_all_mentors_available(self, interaction: discord.Interaction) -> None: + """Command should be executed intially to set all mentors to available""" + mod_role = self.get_role(interaction.guild, "Moderator") + if not mod_role: + await interaction.response.send_message("Error: Could not find the Moderator role.", ephemeral=True) + return + await set_all_mentors_available(mod_role) + await interaction.response.send_message("All mentors are now available.", ephemeral=True) + + + @app_commands.command(name="add_to_thread", description="Add a user to the support thread") + @app_commands.describe(user="The user to add to the thread") + async def add_to_thread(self, interaction: discord.Interaction, user: discord.Member) -> None: + """Add a user to the support thread + + Args: + interaction: The interaction that triggered this command + user: The user to add to the thread + """ + + # first ensure command is being executed in a thread + if not isinstance(interaction.channel, (discord.Thread,)): + await interaction.response.send_message( + "This command can only be used in a support thread.", ephemeral=True + ) + return + # next ensure the thread is in the support channel specifically (check if the thread's parent exists as well) + if not interaction.channel.parent or interaction.channel.parent.name != "support": + await interaction.response.send_message("This command can only be used in the support channel thread.", ephemeral=True) + return + + # check if user is already in the thread + try: + await interaction.channel.fetch_member(user.id) + await interaction.response.send_message(f"{user.mention} is already in this thread.", ephemeral=True) + return + except discord.NotFound: + pass + + try: + await interaction.channel.add_user(user) + await interaction.response.send_message(f"{user.mention} has been added to the thread.", ephemeral=True) + except discord.Forbidden: + await interaction.response.send_message("I don't have permission to add users to this thread.", ephemeral=True) + except Exception as e: + await interaction.response.send_message(f"An error occurred: {str(e)}", ephemeral=True) + + @app_commands.command(name="grant_vc_access", description="Grant a user access to a voice channel") + @app_commands.describe(user="Grant a user access to a voice channel") + @is_mod_slash() + async def grant_vc_access(self, interaction: discord.Interaction, user: discord.Member) -> None: + """Grant a user access to a voice channel + + Args: + interaction: The interaction that triggered this command + user: The user to grant access to + """ + # TODO: We may want to allow this comamnd to be used in any channel since only mods can use it, but then we need to add a parameter to specify the voice channel + # first ensure command is being executed in a voice channel + if not isinstance(interaction.channel, discord.VoiceChannel): + await interaction.response.send_message( + "This command can only be used in a voice channel.", ephemeral=True + ) + return + # next ensure the channel is in the support category specifically + if not interaction.channel.category or interaction.channel.category.name != "Support-VCs": + await interaction.response.send_message('This command can only be used in the "Support-VCs" category.', ephemeral=True) + return + + # check if user already has access to the voice channel + overwrites = interaction.channel.overwrites_for(user) + if overwrites.connect is True: + await interaction.response.send_message(f"{user.mention} already has access to this voice channel.", ephemeral=True) + return + + # try to grant access to the voice channel + try: + await interaction.channel.set_permissions(user, connect=True, view_channel=True) + await interaction.channel.send(f"{user.mention} has been granted access to this voice channel.") + except discord.Forbidden: + await interaction.response.send_message("I don't have permission to grant access to this voice channel.", ephemeral=True) + except Exception as e: + await interaction.response.send_message(f"An error occurred: {str(e)}", ephemeral=True) + async def setup(bot: commands.Bot) -> None: """Add the General cog to the bot diff --git a/apps/discord-bot/cogs/support.py b/apps/discord-bot/cogs/support.py new file mode 100644 index 00000000..07e82710 --- /dev/null +++ b/apps/discord-bot/cogs/support.py @@ -0,0 +1,81 @@ +from discord.ext import commands +from discord import app_commands, Interaction, Embed, Colour +from typing import Literal +from components.ticket_view import TicketView +from utils.checks import is_mod_slash +import discord + +class Support(commands.Cog): + """ + A cog for creating and managing support requests + + This cog includes commands for: + - Creating panels + - Managing support requests and opening threads + - Closing support requests and closing threads + - Viewing support request logs + """ + def __init__(self, bot: commands.Bot) -> None: + """Initialize the Support cog + + Args: + bot: The bot instance + """ + self.bot = bot + + @app_commands.command(name="create_panel", description="Create a support panel") + @app_commands.describe( + title="The title of the support panel", + description="The description of the support panel", + color="Choose the panel's color" + ) + @is_mod_slash() + async def supportpanel( + self, + interaction: Interaction, + title: str, + description: str, + color: Literal["red", "blue", "green", "purple", "orange"] + ) -> None: + """ + Create a support panel mainly used in the #support channel and restricted to @moderators. + + Args: + interaction: The interaction object + title: The title of the support panel + description: The description of the support panel + color: The color of the support panel + """ + color_map = { + "red": Colour.red(), + "blue": Colour.blue(), + "green": Colour.green(), + "purple": Colour.purple(), + "orange": Colour.orange() + } + + mod_role = discord.utils.get(interaction.guild.roles, name="Moderator") + if not mod_role: + await interaction.response.send_message( + "The 'Moderator' role does not exist. Please create it before using this command.", + ephemeral=True + ) + return + + mentors = mod_role.members + + embed = Embed( + title=title, + description=description, + color=color_map[color] + ) + embed.set_footer(text="Powered by SwampHacksXI") + await interaction.response.defer(ephemeral=True) + await interaction.delete_original_response() + await interaction.channel.send(embed=embed, view=TicketView(mentors)) + + + +async def setup(bot: commands.Bot) -> None: + await bot.add_cog(Support(bot)) + diff --git a/apps/discord-bot/components/mentor_ping_state.py b/apps/discord-bot/components/mentor_ping_state.py new file mode 100644 index 00000000..fc5110be --- /dev/null +++ b/apps/discord-bot/components/mentor_ping_state.py @@ -0,0 +1 @@ +last_pinged_mentor_index = 0 \ No newline at end of file diff --git a/apps/discord-bot/components/support_modals.py b/apps/discord-bot/components/support_modals.py new file mode 100644 index 00000000..af263eca --- /dev/null +++ b/apps/discord-bot/components/support_modals.py @@ -0,0 +1,272 @@ +import discord +from discord.ui import Modal, TextInput, View +from discord import TextStyle +from components.support_thread_buttons import SupportThreadButtons, CloseThreadButton +from utils.get_next_thread_name import get_next_thread_name +from utils.get_next_support_vc_name import get_next_support_vc_name +from components.support_vc_buttons import SupportVCButtons +from utils.mentor_functions import get_available_mentors +from components.support_vc_buttons import SupportVCButtons +from components.mentor_ping_state import last_pinged_mentor_index + +class ThreadSupportModal(Modal, title="Support Inquiry"): + """ + Support modal for creating a support thread and embeds. + + This modal provides a form interface for users to submit support requests. + It creates a private thread in the support channel and notifies staff members + through the reports channel. + """ + def __init__(self) -> None: + """ + Initialize the support modal with title and description input fields. + + Creates two text input fields: + - Title: A short title for the support request (max 100 characters) + - Description: A detailed description of the issue (paragraph style) + """ + super().__init__() + self.title_input = TextInput(label="Title", max_length=100) + self.description_input = TextInput(label="Describe your issue", style=TextStyle.paragraph, max_length=1000) + self.add_item(self.title_input) + self.add_item(self.description_input) + + async def on_submit(self, interaction: discord.Interaction) -> None: + """ + Handle the submission of the support request form. + + This method: + 1. Validates the existence of required channels and roles + 2. Creates a private thread in the support channel for the user and moderators to discuss the issue + 3. Sends an embed with the support request details to both the thread and reports channel + 4. Notifies staff members and provides them with a button to join the thread + + Args: + interaction: Discord interaction object + + Note: + The method will send error messages if required channels or roles are not found. + Staff members are notified with a soft ping (hidden mention) in the reports channel. + """ + global last_pinged_mentor_index + reports_channel = discord.utils.get(interaction.guild.channels, name="reports") + support_channel = discord.utils.get(interaction.guild.channels, name="support") + thread_author = interaction.user + mod_role = discord.utils.get(interaction.guild.roles, name="Moderator") + if not mod_role: + await interaction.response.send_message("Error: Could not find the moderator role. Please contact an administrator.", ephemeral=True) + return + + # check if the channels and roles exist + if not reports_channel: + await interaction.response.send_message( + "Error: Could not find the reports channel. Please contact an administrator.", + ephemeral=True + ) + return + if not support_channel: + await interaction.response.send_message("Error: Could not find the support channel. Please contact an administrator.", ephemeral=True) + return + + # truncate description in case it's too long + description = self.description_input.value + shortened_description = "" + if len(description) > 200: + shortened_description = description[:200] + "..." + + # get available mentors + available_mentors = get_available_mentors(mod_role) + if not available_mentors: + await interaction.response.send_message( + "Error: No available mentors at this time. Please try again later.", + ephemeral=True + ) + return + + # implement round-robin pinging of mentors + if last_pinged_mentor_index >= len(available_mentors): + last_pinged_mentor_index = 0 + selected_mentor = available_mentors[last_pinged_mentor_index] + last_pinged_mentor_index = (last_pinged_mentor_index + 1) % len(available_mentors) + action_text = f"{selected_mentor.mention} Please join the thread to assist the user." + print("last_pinged_mentor_index:", last_pinged_mentor_index) + + # create the thread with the next available name and add the initialuser to the thread + thread_name = get_next_thread_name(support_channel) + thread = await support_channel.create_thread( + name=thread_name, + type=discord.ChannelType.private_thread, + reason=f"Support request from {thread_author}", + auto_archive_duration=1440 + ) + await thread.add_user(thread_author) + + + close_button = CloseThreadButton(thread, self.description_input) + close_button_view = View() + close_button_view.add_item(close_button) + + # send initial message as embed in thread with inquiry details + thread_embed = discord.Embed( + title=f"Request: {self.title_input.value}", + description=f"Description: {description}\n\n✅ Thank you for your request, we will be with you shortly!", + color=discord.Color.green(), + ) + await thread.send(embed=thread_embed) + + # create embed for reports channel + reports_embed = discord.Embed( + title=f"💬 New Thread Request: {self.title_input.value}", + description=f"Issue: {shortened_description}\n\nActions: {action_text}", + color=discord.Color.blue(), + ) + reports_embed.add_field(name="Opened by", value=f"{thread_author.mention}\n", inline=True) + + # soft ping staff and send the embed to the reports channel + await reports_channel.send( + content=f"||{mod_role.mention}||", + embed=reports_embed, + view=SupportThreadButtons(thread, self.description_input), + allowed_mentions=discord.AllowedMentions(roles=True) + ) + + await interaction.response.send_message( + f"Thank you! Your issue has been submitted and a thread has been created for you. Please check {thread.mention} for updates.", + ephemeral=True, + ) + +class VCSupportModal(Modal, title="VC Support Inquiry"): + """ + Support modal for creating a support vc and embeds. + + This modal provides a form interface for users to submit support requests. + It creates a private vc in the Support-VCs category and notifies staff members + through the reports channel. + """ + def __init__(self) -> None: + """ + Initialize the support modal with title and description input fields. + + Creates two text input fields: + - Title: A short title for the support request (max 100 characters) + - Description: A detailed description of the issue (paragraph style) + """ + super().__init__() + self.title_input = TextInput(label="Title", max_length=100) + self.description_input = TextInput(label="Describe your issue", style=TextStyle.paragraph, max_length=1000) + self.add_item(self.title_input) + self.add_item(self.description_input) + + async def on_submit(self, interaction: discord.Interaction) -> None: + """ + Handle the submission of the support request form for voice channels. + + This method: + 1. Validates the existence of required channels and roles + 2. Creates a private voice channel in the Support-VCs category for the user and moderators to discuss the issue + 3. Sends an embed with the support request details to both the voice channel and reports channel + 4. Notifies staff members and provides them with a button to join the voice channel + + Args: + interaction: Discord interaction object + + Note: + The method will send error messages if required channels or roles are not found. + Staff members are notified with a soft ping (hidden mention) in the reports channel. + """ + global last_pinged_mentor_index + reports_channel = discord.utils.get(interaction.guild.channels, name="reports") + mod_role = discord.utils.get(interaction.guild.roles, name="Moderator") + category = discord.utils.get(interaction.guild.categories, name="Support-VCs") + vc_author = interaction.user + + if not mod_role: + await interaction.response.send_message("Error: Could not find the moderator role. Please contact an administrator.", ephemeral=True) + return + + if not reports_channel: + await interaction.response.send_message( + "Error: Could not find the reports channel. Please contact an administrator.", + ephemeral=True + ) + return + if category is None: + await interaction.response.send_message("Category 'Support-VCs' not found.", ephemeral=True) + return + + # truncate description in case it's too long + description = self.description_input.value + shortened_description = "" + if len(description) > 200: + shortened_description = description[:200] + "..." + + + # give permissions to the moderator role and the user who clicked the button + overwrites = { + interaction.guild.default_role: discord.PermissionOverwrite(view_channel=False, connect=False), + mod_role: discord.PermissionOverwrite(view_channel=True, connect=True), + vc_author: discord.PermissionOverwrite(view_channel=True, connect=True), + } + + vc_name = get_next_support_vc_name(category) + + # Create the voice channel and get the channel object + voice_channel = await interaction.guild.create_voice_channel( + name=vc_name, + category=category, + user_limit=4, + reason="Support VC created for mentor and inquirer", + overwrites=overwrites + ) + + text_channel_embed = discord.Embed( + title=f"Request: {self.title_input.value}", + description=f"Description: {description}\n\n✅ Thank you for your request, we will be with you shortly!", + color=discord.Color.green(), + ) + + # Try to send a message in the voice channel's chat (if available) + text_channel = interaction.guild.get_channel(voice_channel.id) + if text_channel: + await text_channel.send(content=f"{vc_author.mention}") + await text_channel.send(embed=text_channel_embed) + else: + print("Voice channel does not have an associated text channel.") + return + + # ping the user who created the thread + await interaction.response.send_message( + f"Voice channel created: {voice_channel.mention}", + ephemeral=True + ) + + # # get available mentors + available_mentors = get_available_mentors(mod_role) + if not available_mentors: + await interaction.response.send_message( + "Error: No available mentors at this time. Please try again later.", + ephemeral=True + ) + return + if last_pinged_mentor_index >= len(available_mentors): + last_pinged_mentor_index = 0 + selected_mentor = available_mentors[last_pinged_mentor_index] + last_pinged_mentor_index = (last_pinged_mentor_index + 1) % len(available_mentors) + action_text = f"{selected_mentor.mention} Please join the vc to assist the user." + print("last_pinged_mentor_index:", last_pinged_mentor_index) + + # create embed for reports channel + reports_embed = discord.Embed( + title=f"🎤 New Voice Channel Request: {self.title_input.value}", + description=f"Issue: {shortened_description}\n\nActions: {action_text}", + timestamp=discord.utils.utcnow(), + color=discord.Color.purple() + ) + reports_embed.add_field(name="Opened by", value=f"{vc_author.mention}\n", inline=True) + + await reports_channel.send( + content=f"||{mod_role.mention}||", + embed=reports_embed, + view=SupportVCButtons(voice_channel, self.description_input), + allowed_mentions=discord.AllowedMentions(roles=True) + ) \ No newline at end of file diff --git a/apps/discord-bot/components/support_thread_buttons.py b/apps/discord-bot/components/support_thread_buttons.py new file mode 100644 index 00000000..a37c9664 --- /dev/null +++ b/apps/discord-bot/components/support_thread_buttons.py @@ -0,0 +1,145 @@ +from discord.ui import View, Button +from discord import ButtonStyle, Interaction +import discord +from discord.errors import NotFound +from utils.checks import is_mod_slash +from utils.mentor_functions import set_busy_mentor, set_available_mentor +from components.ticket_state import claimed_tickets + +class SupportThreadButtons(View): + def __init__(self, thread: discord.Thread, description_input: discord.ui.TextInput) -> None: + super().__init__(timeout=None) + self.thread = thread + self.description_input = description_input + self.add_item(ClaimThreadButton(thread, description_input)) + self.add_item(CloseThreadButton(thread, description_input)) + + +class CloseThreadButton(Button): + def __init__(self, thread: discord.Thread, description_input: discord.ui.TextInput): + super().__init__(label="Close Thread", style=ButtonStyle.primary, custom_id="close_thread", emoji="❌") + self.thread = thread + self.description_input = description_input + + async def callback(self, interaction: Interaction): + claimed_tickets.pop(self.thread.id, None) + # print(claimed_tickets) + + # create embed to send to reports channel + reports_channel = discord.utils.get(interaction.guild.channels, name="reports") + if not reports_channel: + await interaction.response.send_message("❌ Reports channel not found.", ephemeral=True) + return + + try: + prefix = "archived-" + title = "" + if interaction.message.embeds: + title = interaction.message.embeds[0].title + trimmed_title = title[22:100 - len(prefix)] + title = trimmed_title + else: + title = self.thread.name + new_name = f"archived-{title}" + await self.thread.edit(name=new_name,archived=True, locked=True) + await interaction.response.send_message(f"Thread: {self.thread.mention} has been archived and locked.", ephemeral=True) + # Set mentor status + await set_available_mentor(interaction.user, True) + await set_busy_mentor(interaction.user, False) + + # edit original message to disable claim button + message = interaction.message + if not message: + await interaction.response.send_message( + "Message not found.", + ephemeral=True + ) + return + new_view = SupportThreadButtons(self.thread, self.description_input) + # disable all buttons in the view + for item in new_view.children: + item.disabled = True + # copy the original embed and update its description + embed = message.embeds[0] if message.embeds else None + + # trim description + description = self.description_input.value + shortened_description = "" + if len(description) > 200: + shortened_description = description[:200] + "..." + else: + shortened_description = description + if embed: + new_embed = embed.copy() + new_embed.description = f"Issue: {shortened_description}\n\nActions: {interaction.user.mention} closed {self.thread.name}." + new_embed.color = discord.Color.red() + await message.edit(embed=new_embed, view=new_view) + else: + await message.edit(view=new_view) + except NotFound: + await interaction.response.send_message( + "This support thread no longer exists.", + ephemeral=True + ) + except Exception as e: + await interaction.response.send_message(f"Failed to archive the support thread. Error: {e}", ephemeral=True) + +class ClaimThreadButton(Button): + def __init__(self, thread: discord.Thread, description_input: discord.ui.TextInput): + super().__init__(label="Claim Thread", style=ButtonStyle.primary, custom_id="claim_thread", emoji="📥") + self.thread = thread + self.description_input = description_input + + async def callback(self, interaction: Interaction): + try: + # Check if the thread is already claimed + if claimed_tickets.get(self.thread.id): + await interaction.response.send_message( + "This thread has already been claimed by another mentor.", + ephemeral=True + ) + return + + # Check if mentor already has an active ticket + if interaction.user.id in claimed_tickets.values(): + await interaction.response.send_message( + "You already have an active support thread or VC. Please close it before claiming a new one.", + ephemeral=True + ) + return + + # Mark as claimed + claimed_tickets[self.thread.id] = interaction.user.id + # print(claimed_tickets) + + + await self.thread.add_user(interaction.user) + await interaction.response.send_message(f"You've been added to the thread: {self.thread.mention}", ephemeral=True) + + await set_available_mentor(interaction.user, False) + await set_busy_mentor(interaction.user, True) + + + # Edit the original message to disable the claim button + message = interaction.message + new_view = SupportThreadButtons(self.thread, self.description_input) + for item in new_view.children: + if isinstance(item, ClaimThreadButton): + item.disabled = True + + # Copy the original embed and update its description + embed = message.embeds[0] if message.embeds else None + if embed: + new_embed = embed.copy() + new_embed.description = f"Issue: {self.description_input.value}\n\nActions: {interaction.user.mention} claimed the thread. Please join the thread to assist the member." + await message.edit(embed=new_embed, view=new_view) + else: + await message.edit(view=new_view) + + except NotFound: + await interaction.response.send_message( + "This support thread no longer exists.", + ephemeral=True + ) + except Exception as e: + await interaction.response.send_message(f"Failed to notify you about the support thread. Error: {e}", ephemeral=True) \ No newline at end of file diff --git a/apps/discord-bot/components/support_vc_buttons.py b/apps/discord-bot/components/support_vc_buttons.py new file mode 100644 index 00000000..5680883e --- /dev/null +++ b/apps/discord-bot/components/support_vc_buttons.py @@ -0,0 +1,126 @@ +from discord.ui import View, Button +from discord import ButtonStyle, Interaction +import discord +from discord.errors import NotFound +from utils.checks import is_mod_slash +from utils.mentor_functions import set_busy_mentor, set_available_mentor +from components.ticket_state import claimed_tickets + +class SupportVCButtons(View): + def __init__(self, voice_channel: discord.VoiceChannel, description_input: discord.ui.TextInput) -> None: + super().__init__(timeout=None) + self.voice_channel = voice_channel + self.description_input = description_input + self.add_item(ClaimTicketButton(voice_channel, description_input)) + self.add_item(CloseTicketButton(voice_channel, description_input)) + + +class CloseTicketButton(Button): + def __init__(self, voice_channel: discord.VoiceChannel, description_input: discord.ui.TextInput): + super().__init__(label="Close Ticket", style=ButtonStyle.primary, custom_id="close_ticket", emoji="❌") + self.voice_channel = voice_channel + self.description_input = description_input + + async def callback(self, interaction: Interaction): + claimed_tickets.pop(self.voice_channel.id, None) + # print(claimed_tickets) + try: + await self.voice_channel.delete() + await interaction.response.send_message(f"Voice channel: {self.voice_channel.mention} has been deleted.", ephemeral=True) + await set_available_mentor(interaction.user, True) + await set_busy_mentor(interaction.user, False) + + # edit original message to disable claim button + message = interaction.message + if not message: + await interaction.response.send_message( + "Message not found.", + ephemeral=True + ) + return + new_view = SupportVCButtons(self.voice_channel, self.description_input) + # disable all buttons in the view + for item in new_view.children: + item.disabled = True + # copy the original embed and update its description + embed = message.embeds[0] if message.embeds else None + if embed: + new_embed = embed.copy() + new_embed.description = f"Issue: {self.description_input.value}\n\nActions: {interaction.user.mention} closed {self.voice_channel.name}." + new_embed.color = discord.Color.red() + await message.edit(embed=new_embed, view=new_view) + else: + await message.edit(view=new_view) + except NotFound: + await interaction.response.send_message( + "This voice channel no longer exists.", + ephemeral=True + ) + except Exception as e: + await interaction.response.send_message(f"Failed to delete the voice channel. Error: {e}", ephemeral=True) + +class ClaimTicketButton(Button): + def __init__(self, voice_channel: discord.VoiceChannel, description_input: discord.ui.TextInput): + super().__init__(label="Claim Ticket", style=ButtonStyle.primary, custom_id="claim_ticket", emoji="📥") + self.voice_channel = voice_channel + self.description_input = description_input + + async def callback(self, interaction: Interaction): + try: + # Check if the ticket is already claimed + if claimed_tickets.get(self.voice_channel.id): + await interaction.response.send_message( + "This ticket has already been claimed by another mentor.", + ephemeral=True + ) + return + + # check if mentor already has an active ticket + if interaction.user.id in claimed_tickets.values(): + await interaction.response.send_message( + "You already have an active support thread or VC. Please close it before claiming a new one.", + ephemeral=True + ) + return + + # Mark as claimed + claimed_tickets[self.voice_channel.id] = interaction.user.id + # print(claimed_tickets) + + + await self.voice_channel.set_permissions( + interaction.user, + connect=True, + view_channel=True + ) + await interaction.response.send_message( + f"Click here to join the voice channel: {self.voice_channel.mention}", + ephemeral=True + ) + await set_available_mentor(interaction.user, False) + await set_busy_mentor(interaction.user, True) + + + # Edit the original message to disable the claim button + message = interaction.message + new_view = SupportVCButtons(self.voice_channel, self.description_input) + for item in new_view.children: + if isinstance(item, ClaimTicketButton): + item.disabled = True + + # Copy the original embed and update its description + embed = message.embeds[0] if message.embeds else None + if embed: + new_embed = embed.copy() + new_embed.description = f"Issue: {self.description_input.value}\n\nActions: {interaction.user.mention} claimed the ticket. Please join the vc to assist the member." + await message.edit(embed=new_embed, view=new_view) + else: + await message.edit(view=new_view) + + except NotFound: + await interaction.response.send_message( + "This voice channel no longer exists.", + ephemeral=True + ) + except Exception as e: + await interaction.response.send_message(f"Failed to notify you about the voice channel. Error: {e}", ephemeral=True) \ No newline at end of file diff --git a/apps/discord-bot/components/ticket_state.py b/apps/discord-bot/components/ticket_state.py new file mode 100644 index 00000000..ebaa327d --- /dev/null +++ b/apps/discord-bot/components/ticket_state.py @@ -0,0 +1 @@ +claimed_tickets = {} # {voice_channel_id: user_id or support_thread_id: user_id} \ No newline at end of file diff --git a/apps/discord-bot/components/ticket_view.py b/apps/discord-bot/components/ticket_view.py new file mode 100644 index 00000000..f14ea590 --- /dev/null +++ b/apps/discord-bot/components/ticket_view.py @@ -0,0 +1,98 @@ +from discord.ui import View, Button, Select +import discord +from discord import Interaction +from components.support_modals import ThreadSupportModal, VCSupportModal + +class TicketView(View): + def __init__(self, mentors=None): + super().__init__(timeout=None) + self.add_item(TicketSelect()) + # self.add_item(MentorSelect(mentors)) + + +class TicketSelect(Select): + def __init__(self): + options = [ + discord.SelectOption(label="Chat in threads", value="thread", emoji="💬", description="Open a support thread"), + discord.SelectOption(label="Chat in VC", value="vc", emoji="🎤", description="Open a private voice chat"), + ] + super().__init__( + placeholder="Choose your support type...", + min_values=1, + max_values=1, + options=options + ) + + async def callback(self, interaction: Interaction): + if self.values[0] == "thread": + try: + await interaction.response.send_modal(ThreadSupportModal()) + + # reset selection when clicked + if interaction.message: + from components.ticket_view import TicketView + await interaction.message.edit(view=TicketView()) + except Exception as e: + await interaction.response.send_message( + "Sorry, there was an error opening the support modal. Please try again later.", + ephemeral=True + ) + print(f"Error in open_threads: {e}") + elif self.values[0] == "vc": + try: + await interaction.response.send_modal(VCSupportModal()) + + # reset selection when clicked + if interaction.message: + from components.ticket_view import TicketView + await interaction.message.edit(view=TicketView()) + except Exception as e: + await interaction.response.send_message( + "Sorry, there was an error opening the support modal. Please try again later.", + ephemeral=True + ) + print(f"Error in open_vc: {e}") + +# This is for selecting mentor dropdown not sure if we will use it yet. +# class MentorSelect(Select): +# """ Select a specific mentor from all mentors. +# """ +# def __init__(self, mentors): +# self.mentors = mentors +# options = [] +# for mentor in mentors: +# options.append( +# discord.SelectOption( +# label=mentor.name, +# value=str(mentor.id), +# emoji="👨‍🏫" if mentor else None +# )) +# super().__init__( +# placeholder="(Optional) Choose a mentor...", +# min_values=1, +# max_values=1, +# options=options +# ) + +# async def callback(self, interaction: Interaction): +# if not self.values: +# await interaction.response.send_message( +# "Please select a mentor to proceed.", +# ephemeral=True +# ) +# return + +# mentor_id = int(self.values[0]) +# mentor = discord.utils.get(self.mentors, id=mentor_id) + +# if mentor: +# await interaction.response.send_message( +# f"You have selected {mentor.name} as your mentor.", +# ephemeral=True +# ) +# else: +# await interaction.response.send_message( +# "Selected mentor is not available. Please try again.", +# ephemeral=True +# ) + diff --git a/apps/discord-bot/config.json b/apps/discord-bot/config.json new file mode 100644 index 00000000..14d6c7b4 --- /dev/null +++ b/apps/discord-bot/config.json @@ -0,0 +1,5 @@ +{ + "roles": { + "moderator": "1376750888953315368" + } +} \ No newline at end of file diff --git a/apps/discord-bot/main.py b/apps/discord-bot/main.py index 4768d3bc..572b7811 100644 --- a/apps/discord-bot/main.py +++ b/apps/discord-bot/main.py @@ -6,6 +6,7 @@ import pathlib import asyncio from typing import Optional +from discord.app_commands import CheckFailure, AppCommandError # Set up logging configuration @@ -16,7 +17,8 @@ mode='w' ) logging.basicConfig( - level=logging.DEBUG, + level=logging.INFO, + #level=logging.DEBUG, handlers=[handler], ) @@ -33,7 +35,30 @@ # Initialize bot with command prefix and intents bot: commands.Bot = commands.Bot(command_prefix='!', intents=intents) -# for testing purposes + +@bot.tree.error +async def on_app_command_error(interaction: discord.Interaction, error: AppCommandError) -> None: + """ + Error handler for app slash commands such as invalid permissions or unexpected errors + + Args: + interaction (discord.Interaction): _description_ + error (AppCommandError): _description_ + + Raises: + error: Error based on CheckFailure (invalid permissions) or unexpected errors + """ + if isinstance(error, CheckFailure): + await interaction.response.send_message( + "🚫 You do not have permission to use this command.", + ephemeral=True + ) + else: + await interaction.response.send_message( + "⚠️ An unexpected error occurred. Please contact an admin.", + ephemeral=True + ) + raise error @bot.event diff --git a/apps/discord-bot/utils/checks.py b/apps/discord-bot/utils/checks.py new file mode 100644 index 00000000..659cf5fc --- /dev/null +++ b/apps/discord-bot/utils/checks.py @@ -0,0 +1,38 @@ +import os +from discord import app_commands, Interaction +import json +from typing import Callable, Coroutine, Any + +def load_config(): + config_path = os.path.join(os.path.dirname(__file__), "..", "config.json") + try: + with open(config_path) as f: + return json.load(f) + except FileNotFoundError: + raise FileNotFoundError(f"Config file not found at {config_path}") + except json.JSONDecodeError as e: + raise ValueError(f"Invalid JSON in config file: {e}") + +config = load_config() + +def is_mod_slash() -> Callable[[Interaction], Coroutine[Any, Any, bool]]: + """ + Check if the user has the moderator role for slash commands. The role is set in the config.json file. + + Returns: + bool: True if the user has the moderator role, False otherwise + """ + async def predicate(interaction: Interaction): + try: + mod_role_id = int(config["roles"]["moderator"]) + except KeyError: + raise ValueError("Moderator role ID not found in config") + except ValueError: + raise ValueError("Invalid moderator role ID in config") + + role_ids = [role.id for role in interaction.user.roles] + + has_permission = mod_role_id in role_ids + return has_permission + + return app_commands.check(predicate) diff --git a/apps/discord-bot/utils/get_next_support_vc_name.py b/apps/discord-bot/utils/get_next_support_vc_name.py new file mode 100644 index 00000000..037f66d5 --- /dev/null +++ b/apps/discord-bot/utils/get_next_support_vc_name.py @@ -0,0 +1,26 @@ +import discord + + +def get_next_support_vc_name(category: discord.CategoryChannel) -> str : + """ Generate the next available support voice channel name in a given category. + + Args: + category (discord.CategoryChannel): The category where the voice channels are located. + + Returns: + str: The next available voice channel name in the format "VC-". + """ + existing_channels = [channel.name for channel in category.voice_channels if channel.name.startswith("VC-")] + + used_numbers = [] + + for vc in existing_channels: + try: + suffix = int(vc.split("-")[1]) + used_numbers.append(suffix) + except (IndexError, ValueError): + continue + + next_number = max(used_numbers, default=0) + 1 + vc_name = f"VC-{next_number}" + return vc_name \ No newline at end of file diff --git a/apps/discord-bot/utils/get_next_thread_name.py b/apps/discord-bot/utils/get_next_thread_name.py new file mode 100644 index 00000000..ac0c06a9 --- /dev/null +++ b/apps/discord-bot/utils/get_next_thread_name.py @@ -0,0 +1,26 @@ +import discord + +def get_next_thread_name(channel: discord.TextChannel) -> str: + """ + Function to get the next thread name for a given channel and return the next thread name + + Args: + channel: Discord text channel object + + Returns: + str: The next thread name + """ + # logic to find next support- name + existing_threads = [t for t in channel.threads if t.name.startswith(f"{channel.name}-")] + used_numbers = [] + + for t in existing_threads: + try: + suffix = int(t.name.split("-")[1]) + used_numbers.append(suffix) + except (IndexError, ValueError): + continue + + next_number = max(used_numbers, default=0) + 1 + thread_name = f"{channel.name}-{next_number}" + return thread_name \ No newline at end of file diff --git a/apps/discord-bot/utils/mentor_functions.py b/apps/discord-bot/utils/mentor_functions.py new file mode 100644 index 00000000..ae8776b1 --- /dev/null +++ b/apps/discord-bot/utils/mentor_functions.py @@ -0,0 +1,54 @@ +import discord + +def get_available_mentors(mod_role: discord.Role) -> list[discord.Member]: + available_mentors = [] + for mentor in mod_role.members: + if "Available Mentor" in [role.name for role in mentor.roles]: + available_mentors.append(mentor) + return available_mentors + +async def set_all_mentors_available(mod_role: discord.Role) -> None: + available_role = discord.utils.get(mentor.guild.roles, name="Available Mentor") + if not available_role: + print("Error: Could not find the 'Available Mentor' role.") + return + for mentor in mod_role.members: + # print(f"Checking mentor: {mentor.name}") + if "Available Mentor" not in [role.name for role in mentor.roles]: + + await mentor.add_roles(available_role) + else: + print(f"{mentor.name} is already an available mentor") + return + + +async def set_busy_mentor(mentor: discord.Member, busy: bool) -> None: + busy_role = discord.utils.get(mentor.guild.roles, name="Busy Mentor") + if not busy_role: + print("Error: Could not find the 'Busy Mentor' role.") + return + if busy == True: + await mentor.add_roles(busy_role) + else: + if busy_role in mentor.roles: + await mentor.remove_roles(busy_role) + else: + print(f"No busy mentor role to remove from {mentor.name}") + return + + +async def set_available_mentor(mentor: discord.Member, available: bool) -> None: + available_role = discord.utils.get(mentor.guild.roles, name="Available Mentor") + if not available_role: + print("Error: Could not find the 'Available Mentor' role.") + return + if available == True: + await mentor.add_roles(available_role) + else: + + if available_role in mentor.roles: + await mentor.remove_roles(available_role) + else: + print(f"No available mentor role to remove from {mentor.name}") + return + \ No newline at end of file From bdcc90acdf9ed6e7e0c7a04bee36d6c7abbf738e Mon Sep 17 00:00:00 2001 From: Hieu Nguyen <76720778+hieunguyent12@users.noreply.github.com> Date: Mon, 14 Jul 2025 08:08:21 -0500 Subject: [PATCH 38/52] feat: added event card (#58) * feat: added event card * fix: full round + more padding * fix: updated buttons for accepted status and event details links * fix: used Link from react-aria-component instead --------- Co-authored-by: Alexander Wang --- apps/web/src/components/ui/Badge/Badge.tsx | 5 +- .../src/components/ui/Card/Card.stories.tsx | 23 +++++ apps/web/src/components/ui/Card/Card.test.tsx | 15 +++ apps/web/src/components/ui/Card/Card.tsx | 30 ++++++ apps/web/src/components/ui/Card/index.ts | 1 + .../ui/Seperator/Seperator.test.tsx | 11 +++ .../src/components/ui/Seperator/Seperator.tsx | 10 ++ apps/web/src/components/ui/Seperator/index.ts | 1 + .../features/Event/components/EventButton.tsx | 9 +- .../features/Event/components/EventCard.tsx | 88 ++++++++++++++++++ .../features/Event/components/placeholder.jpg | Bin 0 -> 82236 bytes .../components/stories/EventCard.stories.tsx | 31 ++++++ apps/web/src/theme.css | 3 + 13 files changed, 223 insertions(+), 4 deletions(-) create mode 100644 apps/web/src/components/ui/Card/Card.stories.tsx create mode 100644 apps/web/src/components/ui/Card/Card.test.tsx create mode 100644 apps/web/src/components/ui/Card/Card.tsx create mode 100644 apps/web/src/components/ui/Card/index.ts create mode 100644 apps/web/src/components/ui/Seperator/Seperator.test.tsx create mode 100644 apps/web/src/components/ui/Seperator/Seperator.tsx create mode 100644 apps/web/src/components/ui/Seperator/index.ts create mode 100644 apps/web/src/features/Event/components/EventCard.tsx create mode 100644 apps/web/src/features/Event/components/placeholder.jpg create mode 100644 apps/web/src/features/Event/components/stories/EventCard.stories.tsx diff --git a/apps/web/src/components/ui/Badge/Badge.tsx b/apps/web/src/components/ui/Badge/Badge.tsx index a2270575..12981324 100644 --- a/apps/web/src/components/ui/Badge/Badge.tsx +++ b/apps/web/src/components/ui/Badge/Badge.tsx @@ -9,7 +9,7 @@ export const badge = tv({ default: "bg-badge-bg-default text-badge-text-default", }, size: { - sm: "px-2 py-1 text-xs", + sm: "px-2.5 py-1 text-xs", md: "px-2.5 py-1.5 text-sm", }, border: { @@ -17,13 +17,14 @@ export const badge = tv({ md: "rounded-md", lg: "rounded-lg", xl: "rounded-xl", + full: "rounded-full", }, }, defaultVariants: { size: "sm", type: "default", - border: "xl", + border: "full", }, }); diff --git a/apps/web/src/components/ui/Card/Card.stories.tsx b/apps/web/src/components/ui/Card/Card.stories.tsx new file mode 100644 index 00000000..3631232a --- /dev/null +++ b/apps/web/src/components/ui/Card/Card.stories.tsx @@ -0,0 +1,23 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { Card } from "."; + +const meta = { + component: Card, + title: "UI/Card", + tags: ["autodocs"], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + children: ( +
+

Title

+

This is the card's description

+
+ ), + size: "sm", + }, +}; diff --git a/apps/web/src/components/ui/Card/Card.test.tsx b/apps/web/src/components/ui/Card/Card.test.tsx new file mode 100644 index 00000000..1ab197ba --- /dev/null +++ b/apps/web/src/components/ui/Card/Card.test.tsx @@ -0,0 +1,15 @@ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import { Card } from "."; + +describe("Card component", () => { + it("renders with correct title", () => { + render( + +

Title

+
, + ); + const card = screen.getByText(/Title/i); + expect(card).toBeInTheDocument(); + }); +}); diff --git a/apps/web/src/components/ui/Card/Card.tsx b/apps/web/src/components/ui/Card/Card.tsx new file mode 100644 index 00000000..de7f16b1 --- /dev/null +++ b/apps/web/src/components/ui/Card/Card.tsx @@ -0,0 +1,30 @@ +/* eslint-disable react-refresh/only-export-components */ +import { forwardRef } from "react"; +import { tv, type VariantProps } from "tailwind-variants"; + +// Very basic card component, maybe in the future we could do something like https://chakra-ui.com/docs/components/card + +export const card = tv({ + base: "inline-block bg-surface rounded-md w-full sm:max-w-96 shadow-xs", + variants: {}, + + defaultVariants: {}, +}); + +type CardVariants = VariantProps; + +export interface CardProps + extends CardVariants, + React.HTMLAttributes {} + +const Card = forwardRef( + ({ className, ...props }, ref) => { + const cardClassName = card({ className }); + + return
; + }, +); + +Card.displayName = "Card"; + +export { Card }; diff --git a/apps/web/src/components/ui/Card/index.ts b/apps/web/src/components/ui/Card/index.ts new file mode 100644 index 00000000..24d32124 --- /dev/null +++ b/apps/web/src/components/ui/Card/index.ts @@ -0,0 +1 @@ +export * from "./Card"; diff --git a/apps/web/src/components/ui/Seperator/Seperator.test.tsx b/apps/web/src/components/ui/Seperator/Seperator.test.tsx new file mode 100644 index 00000000..3e8d01b5 --- /dev/null +++ b/apps/web/src/components/ui/Seperator/Seperator.test.tsx @@ -0,0 +1,11 @@ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import { Separator } from "."; + +describe("Separator component", () => { + it("renders with correct title", () => { + render(); + const separator = screen.getByTestId("separator"); + expect(separator).toBeInTheDocument(); + }); +}); diff --git a/apps/web/src/components/ui/Seperator/Seperator.tsx b/apps/web/src/components/ui/Seperator/Seperator.tsx new file mode 100644 index 00000000..1a601708 --- /dev/null +++ b/apps/web/src/components/ui/Seperator/Seperator.tsx @@ -0,0 +1,10 @@ +const Separator = () => { + return ( +
+ ); +}; + +export { Separator }; diff --git a/apps/web/src/components/ui/Seperator/index.ts b/apps/web/src/components/ui/Seperator/index.ts new file mode 100644 index 00000000..96150241 --- /dev/null +++ b/apps/web/src/components/ui/Seperator/index.ts @@ -0,0 +1 @@ +export * from "./Seperator"; diff --git a/apps/web/src/features/Event/components/EventButton.tsx b/apps/web/src/features/Event/components/EventButton.tsx index 7d7166f9..31cd4a3d 100644 --- a/apps/web/src/features/Event/components/EventButton.tsx +++ b/apps/web/src/features/Event/components/EventButton.tsx @@ -25,9 +25,14 @@ export const eventButton = tv({ interface EventButtonProps extends ButtonProps { status: ApplicationStatusTypes; + text?: string; } -const EventButton = ({ status: statusProp, className }: EventButtonProps) => { +const EventButton = ({ + status: statusProp, + className, + text, +}: EventButtonProps) => { const eventButtonClassName = eventButton({ status: statusProp, className, @@ -35,7 +40,7 @@ const EventButton = ({ status: statusProp, className }: EventButtonProps) => { return ( ); }; diff --git a/apps/web/src/features/Event/components/EventCard.tsx b/apps/web/src/features/Event/components/EventCard.tsx new file mode 100644 index 00000000..002dc47d --- /dev/null +++ b/apps/web/src/features/Event/components/EventCard.tsx @@ -0,0 +1,88 @@ +import TablerCalendarDue from "~icons/tabler/calendar-due"; +import TablerLocation from "~icons/tabler/location"; +import TablerInfoCircle from "~icons/tabler/info-circle"; +import { EventBadge } from "./EventBadge"; +import { EventButton } from "./EventButton"; +import { Link } from "react-aria-components"; + +import imageFile from "./placeholder.jpg"; +import type applicationStatus from "../applicationStatus"; +import { Separator } from "@/components/ui/Seperator"; +import { Card } from "@/components/ui/Card"; + +interface EventCardProps { + eventId: string; + status: keyof typeof applicationStatus; + title: string; + description: string; + date: string; + location: string; +} + +const EventCard = ({ + eventId, + status, + title, + description, + date, + location, +}: EventCardProps) => { + return ( + +
+ {`${title} +
+
+
+

+ {title} +

+ +
+
+

{description}

+
+
+
+ +

{date}

+
+ +
+ +

{location}

+
+ +
+ + + Event Details + +
+
+ + {status === "accepted" ? ( +
+ + +
+ ) : ( + + )} +
+
+ ); +}; + +export { EventCard }; diff --git a/apps/web/src/features/Event/components/placeholder.jpg b/apps/web/src/features/Event/components/placeholder.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1d4de1f0257b81dd96ec257748119336d19ba7cc GIT binary patch literal 82236 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�@k|V@|$$kC=-GW2w8?Sj@D!u-?ji52DfBSO%UT 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

-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 y0+Md3>Sxjd8r<|SbTB>4K^^9!KmXY; + +export default meta; +type Story = StoryObj; + +export const EventCard: Story = { + args: { + eventId: "test id", + status: "rejected", + title: "SwampHacks XI", + description: "UF’s flagship hackathon celebrates its 11th iteration.", + date: "Jan 25 - 26", + location: "Newell Hall", + }, +}; diff --git a/apps/web/src/theme.css b/apps/web/src/theme.css index 99566bbe..62e2087f 100644 --- a/apps/web/src/theme.css +++ b/apps/web/src/theme.css @@ -7,6 +7,7 @@ /* TEXT */ --text-main: var(--color-zinc-900); --text-secondary: var(--color-zinc-600); + --text-link: var(--color-blue-500); /* NAVBAR */ --navlink-bg-inactive: transparent; @@ -127,6 +128,7 @@ /* TEXT */ --text-main: var(--color-zinc-100); --text-secondary: var(--color-zinc-400); + --text-link: var(--color-blue-500); /* NAVBAR */ --navlink-bg-inactive: transparent; @@ -278,6 +280,7 @@ --color-text-main: var(--text-main); --color-text-secondary: var(--text-secondary); + --color-text-link: var(--text-link); --color-navlink-bg-inactive: var(--navlink-bg-inactive); --color-navlink-bg-active: var(--navlink-bg-active); From 0de37b4d6bb9f11712f1bfd51caae55660eabc0a Mon Sep 17 00:00:00 2001 From: Alexander Wang <98280966+AlexanderWangY@users.noreply.github.com> Date: Thu, 17 Jul 2025 13:37:07 -0400 Subject: [PATCH 39/52] feat: swamphacks infra files (#61) Co-authored-by: Alex Wang --- infra/docker-compose.dev.yml | 6 ++++++ infra/docker-compose.yml | 6 ++++++ 2 files changed, 12 insertions(+) create mode 100644 infra/docker-compose.dev.yml create mode 100644 infra/docker-compose.yml diff --git a/infra/docker-compose.dev.yml b/infra/docker-compose.dev.yml new file mode 100644 index 00000000..bb27e91f --- /dev/null +++ b/infra/docker-compose.dev.yml @@ -0,0 +1,6 @@ +services: + api: + image: ghcr.io/swamphacks/core-api:dev + ports: + - "8080:8080" + restart: always diff --git a/infra/docker-compose.yml b/infra/docker-compose.yml new file mode 100644 index 00000000..e6701c5c --- /dev/null +++ b/infra/docker-compose.yml @@ -0,0 +1,6 @@ +services: + api: + image: ghcr.io/swamphacks/core-api:latest + ports: + - "8080:8080" + restart: always From d1f617cf9f7daf98e754f514196ed82bff574bff Mon Sep 17 00:00:00 2001 From: Alexander Wang <98280966+AlexanderWangY@users.noreply.github.com> Date: Thu, 17 Jul 2025 14:07:56 -0400 Subject: [PATCH 40/52] fix: add more build images (#62) --- .github/workflows/build-push-api.yml | 18 ++++++++++++------ infra/docker-compose.dev.yml | 2 ++ infra/docker-compose.yml | 2 ++ 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-push-api.yml b/.github/workflows/build-push-api.yml index e488d9ea..194c2665 100644 --- a/.github/workflows/build-push-api.yml +++ b/.github/workflows/build-push-api.yml @@ -19,6 +19,12 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 + - name: Set up QEMU for cross-platform builds + uses: docker/setup-qemu-action@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + - name: Log in to GitHub Container Registry uses: docker/login-action@v2 with: @@ -26,10 +32,10 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Build Docker image - run: | - docker build -t ghcr.io/${{ github.repository_owner }}/core-api:latest ./apps/api - - - name: Push Docker image + - name: Build and push multi-arch image run: | - docker push ghcr.io/${{ github.repository_owner }}/core-api:latest + docker buildx build \ + --platform linux/amd64,linux/arm64 \ + --push \ + -t ghcr.io/${{ github.repository_owner }}/core-api:latest \ + ./apps/api diff --git a/infra/docker-compose.dev.yml b/infra/docker-compose.dev.yml index bb27e91f..51a1cbc0 100644 --- a/infra/docker-compose.dev.yml +++ b/infra/docker-compose.dev.yml @@ -4,3 +4,5 @@ services: ports: - "8080:8080" restart: always + env_file: + - ./secrets/.env.dev.api diff --git a/infra/docker-compose.yml b/infra/docker-compose.yml index e6701c5c..4c0727cc 100644 --- a/infra/docker-compose.yml +++ b/infra/docker-compose.yml @@ -4,3 +4,5 @@ services: ports: - "8080:8080" restart: always + env_file: + - ./secrets/.env.api From cd7d5afe97188c48ff3392f84b85d3aa741af2de Mon Sep 17 00:00:00 2001 From: Alexander Wang <98280966+AlexanderWangY@users.noreply.github.com> Date: Thu, 17 Jul 2025 14:10:09 -0400 Subject: [PATCH 41/52] Fix/multi platform build (#63) * fix: add more build images * feat: manual workflows --- .github/workflows/build-push-api.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build-push-api.yml b/.github/workflows/build-push-api.yml index 194c2665..52dd85ae 100644 --- a/.github/workflows/build-push-api.yml +++ b/.github/workflows/build-push-api.yml @@ -6,6 +6,7 @@ on: - master paths: - 'apps/api/**' + workflow_dispatch: permissions: contents: read From df35fab674c44993ab6f1c991a7e7c9e104c2386 Mon Sep 17 00:00:00 2001 From: Alexander Wang <98280966+AlexanderWangY@users.noreply.github.com> Date: Fri, 18 Jul 2025 13:21:14 -0400 Subject: [PATCH 42/52] feat: new deployment for prod server (#64) --- .github/workflows/build-push-api.yml | 42 ---------- .github/workflows/prod-build-deploy-api.yml | 92 +++++++++++++++++++++ infra/secrets/README.md | 13 +++ 3 files changed, 105 insertions(+), 42 deletions(-) delete mode 100644 .github/workflows/build-push-api.yml create mode 100644 .github/workflows/prod-build-deploy-api.yml create mode 100644 infra/secrets/README.md diff --git a/.github/workflows/build-push-api.yml b/.github/workflows/build-push-api.yml deleted file mode 100644 index 52dd85ae..00000000 --- a/.github/workflows/build-push-api.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: Build and Push Docker Image to GHCR - -on: - push: - branches: - - master - paths: - - 'apps/api/**' - workflow_dispatch: - -permissions: - contents: read - packages: write - -jobs: - build-and-push: - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Set up QEMU for cross-platform builds - uses: docker/setup-qemu-action@v2 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - - name: Log in to GitHub Container Registry - uses: docker/login-action@v2 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Build and push multi-arch image - run: | - docker buildx build \ - --platform linux/amd64,linux/arm64 \ - --push \ - -t ghcr.io/${{ github.repository_owner }}/core-api:latest \ - ./apps/api diff --git a/.github/workflows/prod-build-deploy-api.yml b/.github/workflows/prod-build-deploy-api.yml new file mode 100644 index 00000000..90e6d5e4 --- /dev/null +++ b/.github/workflows/prod-build-deploy-api.yml @@ -0,0 +1,92 @@ +name: Deploy API to Production + +on: + push: + branches: + - master + paths: + - 'apps/api/**' + workflow_dispatch: + +permissions: + contents: read + packages: write + +concurrency: + group: production-deploy + cancel-in-progress: true + +jobs: + build-and-push: + name: Build and Push Docker Image to GHCR + runs-on: ubuntu-latest + outputs: + image_tag: ${{ steps.set-tag.outputs.image_tag }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up QEMU for cross-platform builds + uses: docker/setup-qemu-action@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push multi-arch image + run: | + docker buildx build \ + --platform linux/amd64,linux/arm64 \ + --push \ + -t ghcr.io/${{ github.repository_owner }}/core-api:latest \ + ./apps/api + + run-migrations: + name: Run Goose Migrations + runs-on: ubuntu-latest + needs: build-and-push + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Goose + run: | + curl -L https://github.com/pressly/goose/releases/latest/download/goose_linux_amd64 --output goose + chmod +x goose + sudo mv goose /usr/local/bin/goose + + - name: Run migrations + run: | + goose -dir ./apps/api/internal/db/migrations postgres "${{ secrets.PROD_DB_URL }}" up + + deploy: + name: Deploy to Production Server + runs-on: ubuntu-latest + needs: [build-and-push, run-migrations] + + steps: + - name: SSH proxy commmand + uses: appleboy/ssh-action@v1 + with: + host: ${{ secrets.SSH_HOST_SWAMPHACKS }} + username: ${{ secrets.SSH_USERNAME_SWAMPHACKS }} + key: ${{ secrets.SSH_KEY_SWAMPHACKS }} + port: ${{ secrets.SSH_PORT_SWAMPHACKS }} + proxy_host: ${{ secrets.SSH_HOST_JUMP }} + proxy_username: ${{ secrets.SSH_USERNAME_JUMP }} + proxy_key: ${{ secrets.SSH_KEY_JUMP }} + proxy_port: ${{ secrets.SSH_PORT_JUMP }} + script: | + cd /home/admin/core/infra + git pull + infisical export --env=prod --format=dotenv --path="/api" --projectId=${{ secrets.INFISICAL_PROJECT_ID }} > ./secrets/.env.api + docker compose pull api + docker compose up -d --no-deps --force-recreate api diff --git a/infra/secrets/README.md b/infra/secrets/README.md new file mode 100644 index 00000000..92cd8248 --- /dev/null +++ b/infra/secrets/README.md @@ -0,0 +1,13 @@ +## Secrets for production and development builds will go in the folder + +The following environments will be generated using the Infiscal CLI tool. What is Infiscal? That is our secret manager! + +- .env.api +- .env.web +- .env.dev.api +- .env.dev.web + +... and more! + +### Add more? +Please let the core maintainers when it comes to adding more env variables! From 12b54c749e3e7145001c4b1b526a590083e65f1c Mon Sep 17 00:00:00 2001 From: Alexander Wang Date: Fri, 18 Jul 2025 13:29:57 -0400 Subject: [PATCH 43/52] hotfix: fix script to indented --- .github/workflows/prod-build-deploy-api.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/prod-build-deploy-api.yml b/.github/workflows/prod-build-deploy-api.yml index 90e6d5e4..74926416 100644 --- a/.github/workflows/prod-build-deploy-api.yml +++ b/.github/workflows/prod-build-deploy-api.yml @@ -84,9 +84,9 @@ jobs: proxy_username: ${{ secrets.SSH_USERNAME_JUMP }} proxy_key: ${{ secrets.SSH_KEY_JUMP }} proxy_port: ${{ secrets.SSH_PORT_JUMP }} - script: | - cd /home/admin/core/infra - git pull - infisical export --env=prod --format=dotenv --path="/api" --projectId=${{ secrets.INFISICAL_PROJECT_ID }} > ./secrets/.env.api - docker compose pull api - docker compose up -d --no-deps --force-recreate api + script: | + cd /home/admin/core/infra + git pull + infisical export --env=prod --format=dotenv --path="/api" --projectId=${{ secrets.INFISICAL_PROJECT_ID }} > ./secrets/.env.api + docker compose pull api + docker compose up -d --no-deps --force-recreate api From a3a02262b4f550c307e16b3efb27b4aacf6e557f Mon Sep 17 00:00:00 2001 From: Alexander Wang Date: Fri, 18 Jul 2025 13:39:58 -0400 Subject: [PATCH 44/52] fix goose migraitons hotfix --- .github/workflows/prod-build-deploy-api.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/prod-build-deploy-api.yml b/.github/workflows/prod-build-deploy-api.yml index 74926416..080e9453 100644 --- a/.github/workflows/prod-build-deploy-api.yml +++ b/.github/workflows/prod-build-deploy-api.yml @@ -59,9 +59,7 @@ jobs: - name: Install Goose run: | - curl -L https://github.com/pressly/goose/releases/latest/download/goose_linux_amd64 --output goose - chmod +x goose - sudo mv goose /usr/local/bin/goose + curl -fsSL https://raw.githubusercontent.com/pressly/goose/master/install.sh | sh - name: Run migrations run: | From 75a77006bfedf269c2e8f123ec861fe60dc4c347 Mon Sep 17 00:00:00 2001 From: Alexander Wang <98280966+AlexanderWangY@users.noreply.github.com> Date: Fri, 18 Jul 2025 14:07:45 -0400 Subject: [PATCH 45/52] feat: dev deployment (#65) --- .github/workflows/dev-build-deploy-api.yml | 89 +++++++++++++++++++++ .github/workflows/prod-build-deploy-api.yml | 1 + infra/docker-compose.dev.yml | 2 +- 3 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/dev-build-deploy-api.yml diff --git a/.github/workflows/dev-build-deploy-api.yml b/.github/workflows/dev-build-deploy-api.yml new file mode 100644 index 00000000..3ae0f9ca --- /dev/null +++ b/.github/workflows/dev-build-deploy-api.yml @@ -0,0 +1,89 @@ +name: Deploy API to Development + +on: + push: + branches: + - dev + paths: + - 'apps/api/**' + - 'infra/docker-compose.dev.yml' + workflow_dispatch: + +permissions: + contents: read + packages: write + +concurrency: + group: dev-deploy + cancel-in-progress: true + +jobs: + build-and-push: + name: Build and Push Docker Image to GHCR + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up QEMU for cross-platform builds + uses: docker/setup-qemu-action@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push multi-arch image + run: | + docker buildx build \ + --platform linux/amd64,linux/arm64 \ + --push \ + -t ghcr.io/${{ github.repository_owner }}/core-api:dev \ + ./apps/api + + run-migrations: + name: Run Goose Migrations + runs-on: ubuntu-latest + needs: build-and-push + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Goose + run: | + curl -fsSL https://raw.githubusercontent.com/pressly/goose/master/install.sh | sh + + - name: Run migrations + run: | + goose -dir ./apps/api/internal/db/migrations postgres "${{ secrets.DEV_DB_URL }}" up + + deploy: + name: Deploy to Production Server + runs-on: ubuntu-latest + needs: [build-and-push, run-migrations] + + steps: + - name: SSH proxy commmand + uses: appleboy/ssh-action@v1 + with: + host: ${{ secrets.SSH_HOST_SWAMPHACKS }} + username: ${{ secrets.SSH_USERNAME_SWAMPHACKS }} + key: ${{ secrets.SSH_KEY_SWAMPHACKS }} + port: ${{ secrets.SSH_PORT_SWAMPHACKS }} + proxy_host: ${{ secrets.SSH_HOST_JUMP }} + proxy_username: ${{ secrets.SSH_USERNAME_JUMP }} + proxy_key: ${{ secrets.SSH_KEY_JUMP }} + proxy_port: ${{ secrets.SSH_PORT_JUMP }} + script: | + cd /home/admin/core/infra + git pull + infisical export --env=dev --format=dotenv --path="/api" --projectId=${{ secrets.INFISICAL_PROJECT_ID }} > ./secrets/.env.dev.api + docker compose -f docker-compose.dev.yml pull api + docker compose -f docker-compose.dev.yml up -d --no-deps --force-recreate api diff --git a/.github/workflows/prod-build-deploy-api.yml b/.github/workflows/prod-build-deploy-api.yml index 080e9453..b95d30ba 100644 --- a/.github/workflows/prod-build-deploy-api.yml +++ b/.github/workflows/prod-build-deploy-api.yml @@ -6,6 +6,7 @@ on: - master paths: - 'apps/api/**' + - 'infra/docker-compose.yml' workflow_dispatch: permissions: diff --git a/infra/docker-compose.dev.yml b/infra/docker-compose.dev.yml index 51a1cbc0..75c1a58a 100644 --- a/infra/docker-compose.dev.yml +++ b/infra/docker-compose.dev.yml @@ -2,7 +2,7 @@ services: api: image: ghcr.io/swamphacks/core-api:dev ports: - - "8080:8080" + - "8081:8080" restart: always env_file: - ./secrets/.env.dev.api From cad1a11ddd1955806663408b5108c4767404d5a8 Mon Sep 17 00:00:00 2001 From: Alexander Wang <98280966+AlexanderWangY@users.noreply.github.com> Date: Fri, 18 Jul 2025 14:20:37 -0400 Subject: [PATCH 46/52] Feat/dev deployment api (#66) * feat: dev deployment * fix: add the rigth deployment names --- .github/workflows/dev-build-deploy-api.yml | 5 ++++- .github/workflows/prod-build-deploy-api.yml | 3 +++ infra/docker-compose.dev.yml | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dev-build-deploy-api.yml b/.github/workflows/dev-build-deploy-api.yml index 3ae0f9ca..e2eff054 100644 --- a/.github/workflows/dev-build-deploy-api.yml +++ b/.github/workflows/dev-build-deploy-api.yml @@ -65,7 +65,7 @@ jobs: goose -dir ./apps/api/internal/db/migrations postgres "${{ secrets.DEV_DB_URL }}" up deploy: - name: Deploy to Production Server + name: Deploy to Development Server runs-on: ubuntu-latest needs: [build-and-push, run-migrations] @@ -83,6 +83,9 @@ jobs: proxy_port: ${{ secrets.SSH_PORT_JUMP }} script: | cd /home/admin/core/infra + git fetch + git checkout dev + git reset --hard origin/dev git pull infisical export --env=dev --format=dotenv --path="/api" --projectId=${{ secrets.INFISICAL_PROJECT_ID }} > ./secrets/.env.dev.api docker compose -f docker-compose.dev.yml pull api diff --git a/.github/workflows/prod-build-deploy-api.yml b/.github/workflows/prod-build-deploy-api.yml index b95d30ba..8250e992 100644 --- a/.github/workflows/prod-build-deploy-api.yml +++ b/.github/workflows/prod-build-deploy-api.yml @@ -85,6 +85,9 @@ jobs: proxy_port: ${{ secrets.SSH_PORT_JUMP }} script: | cd /home/admin/core/infra + git fetch + git checkout master + git reset --hard origin/master git pull infisical export --env=prod --format=dotenv --path="/api" --projectId=${{ secrets.INFISICAL_PROJECT_ID }} > ./secrets/.env.api docker compose pull api diff --git a/infra/docker-compose.dev.yml b/infra/docker-compose.dev.yml index 75c1a58a..c55fcec8 100644 --- a/infra/docker-compose.dev.yml +++ b/infra/docker-compose.dev.yml @@ -1,5 +1,5 @@ services: - api: + dev-api: image: ghcr.io/swamphacks/core-api:dev ports: - "8081:8080" From b838276abdc1e76376fe4e0e3c67173d3946c4c0 Mon Sep 17 00:00:00 2001 From: Alexander Wang Date: Fri, 18 Jul 2025 14:29:30 -0400 Subject: [PATCH 47/52] hotfix: dev api --- .github/workflows/dev-build-deploy-api.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dev-build-deploy-api.yml b/.github/workflows/dev-build-deploy-api.yml index e2eff054..95a663e1 100644 --- a/.github/workflows/dev-build-deploy-api.yml +++ b/.github/workflows/dev-build-deploy-api.yml @@ -88,5 +88,5 @@ jobs: git reset --hard origin/dev git pull infisical export --env=dev --format=dotenv --path="/api" --projectId=${{ secrets.INFISICAL_PROJECT_ID }} > ./secrets/.env.dev.api - docker compose -f docker-compose.dev.yml pull api - docker compose -f docker-compose.dev.yml up -d --no-deps --force-recreate api + docker compose -f docker-compose.dev.yml pull dev-api + docker compose -f docker-compose.dev.yml up -d --no-deps --force-recreate dev-api From 78e3772acab9996eeac763ec3ac42cbc81f214f3 Mon Sep 17 00:00:00 2001 From: Alexander Wang <98280966+AlexanderWangY@users.noreply.github.com> Date: Fri, 18 Jul 2025 14:41:15 -0400 Subject: [PATCH 48/52] Merge and fastforward timeline (#68) * feat: dev deployment (#65) * Feat/dev deployment api (#66) * feat: dev deployment * fix: add the rigth deployment names * hotfix: dev api From ae3b57ce9bdd6a316c311d7dcf39fb50a36afca8 Mon Sep 17 00:00:00 2001 From: h1divp <71522316+h1divp@users.noreply.github.com> Date: Mon, 21 Jul 2025 15:43:52 -0400 Subject: [PATCH 49/52] Refactor: made UpdateEvent sql easier to use, renamed query. Fix: removed GetEventByLocation --- apps/api/internal/db/queries/events.sql | 32 +++--- apps/api/internal/db/sqlc/events.sql.go | 132 ++++++------------------ apps/api/internal/db/sqlc/querier.go | 3 +- 3 files changed, 47 insertions(+), 120 deletions(-) diff --git a/apps/api/internal/db/queries/events.sql b/apps/api/internal/db/queries/events.sql index 7636e5fa..1177de00 100644 --- a/apps/api/internal/db/queries/events.sql +++ b/apps/api/internal/db/queries/events.sql @@ -14,26 +14,22 @@ RETURNING *; SELECT * FROM events WHERE id = $1; --- name: GetEventByLocation :many -SELECT * FROM events -WHERE location = $1; - --- name: UpdateEvent :exec +-- name: UpdateEventById :exec UPDATE events SET - name = CASE WHEN @name_do_update::boolean THEN @name ELSE name END, - description = CASE WHEN @description_do_update::boolean THEN @description ELSE description END, - location = CASE WHEN @location_do_update::boolean THEN @location ELSE location END, - location_url = CASE WHEN @location_url_do_update::boolean THEN @location_url ELSE location_url END, - max_attendees = CASE WHEN @max_attendees_do_update::boolean THEN @max_attendees ELSE max_attendees END, - application_open = CASE WHEN @application_open_do_update::boolean THEN @application_open ELSE application_open END, - application_close = CASE WHEN @application_close_do_update::boolean THEN @application_close ELSE application_close END, - rsvp_deadline = CASE WHEN @rsvp_deadline_do_update::boolean THEN @rsvp_deadline ELSE rsvp_deadline END, - decision_release = CASE WHEN @decision_release_do_update::boolean THEN @decision_release ELSE decision_release END, - 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, - website_url = CASE WHEN @website_url_do_update::boolean THEN @website_url ELSE website_url END, - is_published = CASE WHEN @is_published_do_update::boolean THEN @is_published ELSE is_published END + name = coalesce(sqlc.narg('name'), name), + description = coalesce(sqlc.narg('description'), description), + location = coalesce(sqlc.narg('location'), location), + location_url = coalesce(sqlc.narg('location_url'), location_url), + max_attendees = coalesce(sqlc.narg('max_attendees'), max_attendees), + application_open = coalesce(sqlc.narg('application_open'), application_open), + application_close = coalesce(sqlc.narg('application_close'), application_close), + rsvp_deadline = coalesce(sqlc.narg('rsvp_deadline'), rsvp_deadline), + decision_release = coalesce(sqlc.narg('decision_release'), decision_release), + start_time = coalesce(sqlc.narg('start_time'), start_time), + end_time = coalesce(sqlc.narg('end_time'), end_time), + website_url = coalesce(sqlc.narg('website_url'), website_url), + is_published = coalesce(sqlc.narg('is_published'), is_published) WHERE id = @id::uuid; diff --git a/apps/api/internal/db/sqlc/events.sql.go b/apps/api/internal/db/sqlc/events.sql.go index 72841755..a4cda436 100644 --- a/apps/api/internal/db/sqlc/events.sql.go +++ b/apps/api/internal/db/sqlc/events.sql.go @@ -102,125 +102,57 @@ func (q *Queries) GetEventByID(ctx context.Context, id uuid.UUID) (Event, error) return i, err } -const getEventByLocation = `-- name: GetEventByLocation :many -SELECT id, name, description, location, location_url, max_attendees, application_open, application_close, rsvp_deadline, decision_release, start_time, end_time, website_url, is_published, created_at, updated_at FROM events -WHERE location = $1 -` - -func (q *Queries) GetEventByLocation(ctx context.Context, location *string) ([]Event, error) { - rows, err := q.db.Query(ctx, getEventByLocation, location) - if err != nil { - return nil, err - } - defer rows.Close() - items := []Event{} - for rows.Next() { - var i Event - if err := rows.Scan( - &i.ID, - &i.Name, - &i.Description, - &i.Location, - &i.LocationUrl, - &i.MaxAttendees, - &i.ApplicationOpen, - &i.ApplicationClose, - &i.RsvpDeadline, - &i.DecisionRelease, - &i.StartTime, - &i.EndTime, - &i.WebsiteUrl, - &i.IsPublished, - &i.CreatedAt, - &i.UpdatedAt, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const updateEvent = `-- name: UpdateEvent :exec +const updateEventById = `-- name: UpdateEventById :exec UPDATE events SET - name = CASE WHEN $1::boolean THEN $2 ELSE name END, - description = CASE WHEN $3::boolean THEN $4 ELSE description END, - location = CASE WHEN $5::boolean THEN $6 ELSE location END, - location_url = CASE WHEN $7::boolean THEN $8 ELSE location_url END, - max_attendees = CASE WHEN $9::boolean THEN $10 ELSE max_attendees END, - application_open = CASE WHEN $11::boolean THEN $12 ELSE application_open END, - application_close = CASE WHEN $13::boolean THEN $14 ELSE application_close END, - rsvp_deadline = CASE WHEN $15::boolean THEN $16 ELSE rsvp_deadline END, - decision_release = CASE WHEN $17::boolean THEN $18 ELSE decision_release END, - start_time = CASE WHEN $19::boolean THEN $20 ELSE start_time END, - end_time = CASE WHEN $21::boolean THEN $22 ELSE end_time END, - website_url = CASE WHEN $23::boolean THEN $24 ELSE website_url END, - is_published = CASE WHEN $25::boolean THEN $26 ELSE is_published END + name = coalesce($1, name), + description = coalesce($2, description), + location = coalesce($3, location), + location_url = coalesce($4, location_url), + max_attendees = coalesce($5, max_attendees), + application_open = coalesce($6, application_open), + application_close = coalesce($7, application_close), + rsvp_deadline = coalesce($8, rsvp_deadline), + decision_release = coalesce($9, decision_release), + start_time = coalesce($10, start_time), + end_time = coalesce($11, end_time), + website_url = coalesce($12, website_url), + is_published = coalesce($13, is_published) WHERE - id = $27::uuid + id = $14::uuid ` -type UpdateEventParams struct { - NameDoUpdate bool `json:"name_do_update"` - Name string `json:"name"` - DescriptionDoUpdate bool `json:"description_do_update"` - Description *string `json:"description"` - LocationDoUpdate bool `json:"location_do_update"` - Location *string `json:"location"` - LocationUrlDoUpdate bool `json:"location_url_do_update"` - LocationUrl *string `json:"location_url"` - MaxAttendeesDoUpdate bool `json:"max_attendees_do_update"` - MaxAttendees *int32 `json:"max_attendees"` - ApplicationOpenDoUpdate bool `json:"application_open_do_update"` - ApplicationOpen time.Time `json:"application_open"` - ApplicationCloseDoUpdate bool `json:"application_close_do_update"` - ApplicationClose time.Time `json:"application_close"` - RsvpDeadlineDoUpdate bool `json:"rsvp_deadline_do_update"` - RsvpDeadline *time.Time `json:"rsvp_deadline"` - DecisionReleaseDoUpdate bool `json:"decision_release_do_update"` - DecisionRelease *time.Time `json:"decision_release"` - StartTimeDoUpdate bool `json:"start_time_do_update"` - StartTime time.Time `json:"start_time"` - EndTimeDoUpdate bool `json:"end_time_do_update"` - EndTime time.Time `json:"end_time"` - WebsiteUrlDoUpdate bool `json:"website_url_do_update"` - WebsiteUrl *string `json:"website_url"` - IsPublishedDoUpdate bool `json:"is_published_do_update"` - IsPublished *bool `json:"is_published"` - ID uuid.UUID `json:"id"` +type UpdateEventByIdParams struct { + Name *string `json:"name"` + Description *string `json:"description"` + Location *string `json:"location"` + LocationUrl *string `json:"location_url"` + MaxAttendees *int32 `json:"max_attendees"` + ApplicationOpen *time.Time `json:"application_open"` + ApplicationClose *time.Time `json:"application_close"` + RsvpDeadline *time.Time `json:"rsvp_deadline"` + DecisionRelease *time.Time `json:"decision_release"` + StartTime *time.Time `json:"start_time"` + EndTime *time.Time `json:"end_time"` + WebsiteUrl *string `json:"website_url"` + IsPublished *bool `json:"is_published"` + ID uuid.UUID `json:"id"` } -func (q *Queries) UpdateEvent(ctx context.Context, arg UpdateEventParams) error { - _, err := q.db.Exec(ctx, updateEvent, - arg.NameDoUpdate, +func (q *Queries) UpdateEventById(ctx context.Context, arg UpdateEventByIdParams) error { + _, err := q.db.Exec(ctx, updateEventById, arg.Name, - arg.DescriptionDoUpdate, arg.Description, - arg.LocationDoUpdate, arg.Location, - arg.LocationUrlDoUpdate, arg.LocationUrl, - arg.MaxAttendeesDoUpdate, arg.MaxAttendees, - arg.ApplicationOpenDoUpdate, arg.ApplicationOpen, - arg.ApplicationCloseDoUpdate, arg.ApplicationClose, - arg.RsvpDeadlineDoUpdate, arg.RsvpDeadline, - arg.DecisionReleaseDoUpdate, arg.DecisionRelease, - arg.StartTimeDoUpdate, arg.StartTime, - arg.EndTimeDoUpdate, arg.EndTime, - arg.WebsiteUrlDoUpdate, arg.WebsiteUrl, - arg.IsPublishedDoUpdate, arg.IsPublished, arg.ID, ) diff --git a/apps/api/internal/db/sqlc/querier.go b/apps/api/internal/db/sqlc/querier.go index 1df6a562..65bbba66 100644 --- a/apps/api/internal/db/sqlc/querier.go +++ b/apps/api/internal/db/sqlc/querier.go @@ -30,7 +30,6 @@ type Querier interface { GetByProviderAndAccountID(ctx context.Context, arg GetByProviderAndAccountIDParams) (AuthAccount, error) GetByUserID(ctx context.Context, userID uuid.UUID) ([]AuthAccount, error) GetEventByID(ctx context.Context, id uuid.UUID) (Event, error) - GetEventByLocation(ctx context.Context, location *string) ([]Event, error) GetSessionByID(ctx context.Context, id uuid.UUID) (AuthSession, error) GetSessionsByUserID(ctx context.Context, userID uuid.UUID) ([]AuthSession, error) GetUserByEmail(ctx context.Context, email *string) (AuthUser, error) @@ -38,7 +37,7 @@ type Querier interface { InvalidateSessionByID(ctx context.Context, id uuid.UUID) error TouchSession(ctx context.Context, arg TouchSessionParams) error UpdateApplication(ctx context.Context, arg UpdateApplicationParams) error - UpdateEvent(ctx context.Context, arg UpdateEventParams) error + UpdateEventById(ctx context.Context, arg UpdateEventByIdParams) error UpdateSessionExpiration(ctx context.Context, arg UpdateSessionExpirationParams) error UpdateTokens(ctx context.Context, arg UpdateTokensParams) error UpdateUser(ctx context.Context, arg UpdateUserParams) error From 7b7ef87736b99560c9bd47c7f5142b35ac35129e Mon Sep 17 00:00:00 2001 From: h1divp <71522316+h1divp@users.noreply.github.com> Date: Mon, 21 Jul 2025 15:53:02 -0400 Subject: [PATCH 50/52] feat: added saved_at param and added to UpdateEventById --- .../internal/db/migrations/20250619161938_event_schema.sql | 1 + apps/api/internal/db/queries/events.sql | 3 ++- apps/api/internal/db/sqlc/events.sql.go | 6 ++++-- apps/api/internal/db/sqlc/models.go | 1 + 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/apps/api/internal/db/migrations/20250619161938_event_schema.sql b/apps/api/internal/db/migrations/20250619161938_event_schema.sql index f8a5b4d2..997e7ea9 100644 --- a/apps/api/internal/db/migrations/20250619161938_event_schema.sql +++ b/apps/api/internal/db/migrations/20250619161938_event_schema.sql @@ -21,6 +21,7 @@ CREATE TABLE events ( -- Metadata website_url TEXT, is_published BOOLEAN DEFAULT FALSE, + saved_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() diff --git a/apps/api/internal/db/queries/events.sql b/apps/api/internal/db/queries/events.sql index 1177de00..55c57897 100644 --- a/apps/api/internal/db/queries/events.sql +++ b/apps/api/internal/db/queries/events.sql @@ -29,7 +29,8 @@ SET start_time = coalesce(sqlc.narg('start_time'), start_time), end_time = coalesce(sqlc.narg('end_time'), end_time), website_url = coalesce(sqlc.narg('website_url'), website_url), - is_published = coalesce(sqlc.narg('is_published'), is_published) + is_published = coalesce(sqlc.narg('is_published'), is_published), + saved_at = coalesce(sqlc.narg('saved_at'), is_published) WHERE id = @id::uuid; diff --git a/apps/api/internal/db/sqlc/events.sql.go b/apps/api/internal/db/sqlc/events.sql.go index a4cda436..ed104616 100644 --- a/apps/api/internal/db/sqlc/events.sql.go +++ b/apps/api/internal/db/sqlc/events.sql.go @@ -22,7 +22,7 @@ INSERT INTO events ( $2, $3, $4, $5 ) -RETURNING id, name, description, location, location_url, max_attendees, application_open, application_close, rsvp_deadline, decision_release, start_time, end_time, website_url, is_published, created_at, updated_at +RETURNING id, name, description, location, location_url, max_attendees, application_open, application_close, rsvp_deadline, decision_release, start_time, end_time, website_url, is_published, saved_at, created_at, updated_at ` type CreateEventParams struct { @@ -57,6 +57,7 @@ func (q *Queries) CreateEvent(ctx context.Context, arg CreateEventParams) (Event &i.EndTime, &i.WebsiteUrl, &i.IsPublished, + &i.SavedAt, &i.CreatedAt, &i.UpdatedAt, ) @@ -74,7 +75,7 @@ func (q *Queries) DeleteEvent(ctx context.Context, id uuid.UUID) error { } const getEventByID = `-- name: GetEventByID :one -SELECT id, name, description, location, location_url, max_attendees, application_open, application_close, rsvp_deadline, decision_release, start_time, end_time, website_url, is_published, created_at, updated_at FROM events +SELECT id, name, description, location, location_url, max_attendees, application_open, application_close, rsvp_deadline, decision_release, start_time, end_time, website_url, is_published, saved_at, created_at, updated_at FROM events WHERE id = $1 ` @@ -96,6 +97,7 @@ func (q *Queries) GetEventByID(ctx context.Context, id uuid.UUID) (Event, error) &i.EndTime, &i.WebsiteUrl, &i.IsPublished, + &i.SavedAt, &i.CreatedAt, &i.UpdatedAt, ) diff --git a/apps/api/internal/db/sqlc/models.go b/apps/api/internal/db/sqlc/models.go index 159ac243..2ca6d713 100644 --- a/apps/api/internal/db/sqlc/models.go +++ b/apps/api/internal/db/sqlc/models.go @@ -210,6 +210,7 @@ type Event struct { EndTime time.Time `json:"end_time"` WebsiteUrl *string `json:"website_url"` IsPublished *bool `json:"is_published"` + SavedAt *time.Time `json:"saved_at"` CreatedAt *time.Time `json:"created_at"` UpdatedAt *time.Time `json:"updated_at"` } From 26f3975d982d0e3499d13ca99928354191c97abb Mon Sep 17 00:00:00 2001 From: h1divp <71522316+h1divp@users.noreply.github.com> Date: Mon, 21 Jul 2025 15:54:26 -0400 Subject: [PATCH 51/52] chore: make generate --- apps/api/internal/db/sqlc/events.sql.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/api/internal/db/sqlc/events.sql.go b/apps/api/internal/db/sqlc/events.sql.go index ed104616..25b8de7a 100644 --- a/apps/api/internal/db/sqlc/events.sql.go +++ b/apps/api/internal/db/sqlc/events.sql.go @@ -119,9 +119,10 @@ SET start_time = coalesce($10, start_time), end_time = coalesce($11, end_time), website_url = coalesce($12, website_url), - is_published = coalesce($13, is_published) + is_published = coalesce($13, is_published), + saved_at = coalesce($14, is_published) WHERE - id = $14::uuid + id = $15::uuid ` type UpdateEventByIdParams struct { @@ -138,6 +139,7 @@ type UpdateEventByIdParams struct { EndTime *time.Time `json:"end_time"` WebsiteUrl *string `json:"website_url"` IsPublished *bool `json:"is_published"` + SavedAt *time.Time `json:"saved_at"` ID uuid.UUID `json:"id"` } @@ -156,6 +158,7 @@ func (q *Queries) UpdateEventById(ctx context.Context, arg UpdateEventByIdParams arg.EndTime, arg.WebsiteUrl, arg.IsPublished, + arg.SavedAt, arg.ID, ) return err From 5f8aa5c9e09c16ebdc79a85bba617dc1f14d7443 Mon Sep 17 00:00:00 2001 From: Phoenix <71522316+h1divp@users.noreply.github.com> Date: Mon, 21 Jul 2025 16:39:34 -0400 Subject: [PATCH 52/52] Revert "TECH-123: Create sql query to fetch event, user, and application information" --- apps/api/Makefile | 4 +- .../20250619161938_event_schema.sql | 2 +- apps/api/internal/db/queries/applications.sql | 24 --- apps/api/internal/db/queries/events.sql | 39 ----- apps/api/internal/db/sqlc/applications.sql.go | 118 ------------- apps/api/internal/db/sqlc/events.sql.go | 165 ------------------ apps/api/internal/db/sqlc/models.go | 1 - apps/api/internal/db/sqlc/querier.go | 8 - apps/docs/mkdocs.yml | 1 - apps/docs/src/api/db_testing.md | 30 ---- apps/docs/src/api/installation.md | 14 +- docker-compose.yml | 2 - 12 files changed, 9 insertions(+), 399 deletions(-) delete mode 100644 apps/api/internal/db/queries/applications.sql delete mode 100644 apps/api/internal/db/queries/events.sql delete mode 100644 apps/api/internal/db/sqlc/applications.sql.go delete mode 100644 apps/api/internal/db/sqlc/events.sql.go delete mode 100644 apps/docs/src/api/db_testing.md diff --git a/apps/api/Makefile b/apps/api/Makefile index 84d5b63d..820dad83 100644 --- a/apps/api/Makefile +++ b/apps/api/Makefile @@ -1,10 +1,10 @@ include .env.dev migrate-up: - @goose -dir ./internal/db/migrations postgres ${DATABASE_URL_MIGRATION} up + @goose -dir ./internal/db/migrations postgres ${DATABASE_URL_MIGRATIONS} up migrate-down: - @goose -dir ./internal/db/migrations postgres ${DATABASE_URL_MIGRATION} down + @goose -dir ./internal/db/migrations postgres ${DATABASE_URL_MIGRATIONS} down generate: @sqlc generate diff --git a/apps/api/internal/db/migrations/20250619161938_event_schema.sql b/apps/api/internal/db/migrations/20250619161938_event_schema.sql index 997e7ea9..ddb8929a 100644 --- a/apps/api/internal/db/migrations/20250619161938_event_schema.sql +++ b/apps/api/internal/db/migrations/20250619161938_event_schema.sql @@ -13,6 +13,7 @@ CREATE TABLE events ( application_close TIMESTAMPTZ NOT NULL, rsvp_deadline TIMESTAMPTZ, decision_release TIMESTAMPTZ, + -- Event phase start_time TIMESTAMPTZ NOT NULL, @@ -21,7 +22,6 @@ CREATE TABLE events ( -- Metadata website_url TEXT, is_published BOOLEAN DEFAULT FALSE, - saved_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() diff --git a/apps/api/internal/db/queries/applications.sql b/apps/api/internal/db/queries/applications.sql deleted file mode 100644 index 2e553de7..00000000 --- a/apps/api/internal/db/queries/applications.sql +++ /dev/null @@ -1,24 +0,0 @@ --- name: CreateApplication :one -INSERT INTO applications ( - user_id, event_id -) VALUES ( - $1, $2 -) -RETURNING *; - --- name: GetApplicationByUserAndEventID :one -SELECT * FROM applications -WHERE user_id = $1 AND event_id = $2; - --- name: UpdateApplication :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, - resume_url = CASE WHEN @resume_url_do_update::boolean THEN @resume_url ELSE resume_url END -WHERE - user_id = @user_id AND event_id = @event_id; - --- name: DeleteApplication :exec -DELETE FROM applications -WHERE user_id = $1 AND event_id = $2; diff --git a/apps/api/internal/db/queries/events.sql b/apps/api/internal/db/queries/events.sql deleted file mode 100644 index 55c57897..00000000 --- a/apps/api/internal/db/queries/events.sql +++ /dev/null @@ -1,39 +0,0 @@ --- name: CreateEvent :one -INSERT INTO events ( - name, - application_open, application_close, - start_time, end_time -) VALUES ( - $1, - $2, $3, - $4, $5 -) -RETURNING *; - --- name: GetEventByID :one -SELECT * FROM events -WHERE id = $1; - --- name: UpdateEventById :exec -UPDATE events -SET - name = coalesce(sqlc.narg('name'), name), - description = coalesce(sqlc.narg('description'), description), - location = coalesce(sqlc.narg('location'), location), - location_url = coalesce(sqlc.narg('location_url'), location_url), - max_attendees = coalesce(sqlc.narg('max_attendees'), max_attendees), - application_open = coalesce(sqlc.narg('application_open'), application_open), - application_close = coalesce(sqlc.narg('application_close'), application_close), - rsvp_deadline = coalesce(sqlc.narg('rsvp_deadline'), rsvp_deadline), - decision_release = coalesce(sqlc.narg('decision_release'), decision_release), - start_time = coalesce(sqlc.narg('start_time'), start_time), - end_time = coalesce(sqlc.narg('end_time'), end_time), - website_url = coalesce(sqlc.narg('website_url'), website_url), - is_published = coalesce(sqlc.narg('is_published'), is_published), - saved_at = coalesce(sqlc.narg('saved_at'), is_published) -WHERE - id = @id::uuid; - --- name: DeleteEvent :exec -DELETE FROM events -WHERE id = $1; diff --git a/apps/api/internal/db/sqlc/applications.sql.go b/apps/api/internal/db/sqlc/applications.sql.go deleted file mode 100644 index f8d9a23b..00000000 --- a/apps/api/internal/db/sqlc/applications.sql.go +++ /dev/null @@ -1,118 +0,0 @@ -// Code generated by sqlc. DO NOT EDIT. -// versions: -// sqlc v1.29.0 -// source: applications.sql - -package sqlc - -import ( - "context" - - "github.com/google/uuid" -) - -const createApplication = `-- name: CreateApplication :one -INSERT INTO applications ( - user_id, event_id -) VALUES ( - $1, $2 -) -RETURNING user_id, event_id, status, application, resume_url, created_at, saved_at, updated_at -` - -type CreateApplicationParams struct { - UserID uuid.UUID `json:"user_id"` - EventID uuid.UUID `json:"event_id"` -} - -func (q *Queries) CreateApplication(ctx context.Context, arg CreateApplicationParams) (Application, error) { - row := q.db.QueryRow(ctx, createApplication, arg.UserID, arg.EventID) - var i Application - err := row.Scan( - &i.UserID, - &i.EventID, - &i.Status, - &i.Application, - &i.ResumeUrl, - &i.CreatedAt, - &i.SavedAt, - &i.UpdatedAt, - ) - return i, err -} - -const deleteApplication = `-- name: DeleteApplication :exec -DELETE FROM applications -WHERE user_id = $1 AND event_id = $2 -` - -type DeleteApplicationParams struct { - UserID uuid.UUID `json:"user_id"` - EventID uuid.UUID `json:"event_id"` -} - -func (q *Queries) DeleteApplication(ctx context.Context, arg DeleteApplicationParams) error { - _, err := q.db.Exec(ctx, deleteApplication, arg.UserID, arg.EventID) - return err -} - -const getApplicationByUserAndEventID = `-- name: GetApplicationByUserAndEventID :one -SELECT user_id, event_id, status, application, resume_url, created_at, saved_at, updated_at FROM applications -WHERE user_id = $1 AND event_id = $2 -` - -type GetApplicationByUserAndEventIDParams struct { - UserID uuid.UUID `json:"user_id"` - EventID uuid.UUID `json:"event_id"` -} - -func (q *Queries) GetApplicationByUserAndEventID(ctx context.Context, arg GetApplicationByUserAndEventIDParams) (Application, error) { - row := q.db.QueryRow(ctx, getApplicationByUserAndEventID, arg.UserID, arg.EventID) - var i Application - err := row.Scan( - &i.UserID, - &i.EventID, - &i.Status, - &i.Application, - &i.ResumeUrl, - &i.CreatedAt, - &i.SavedAt, - &i.UpdatedAt, - ) - return i, err -} - -const updateApplication = `-- name: UpdateApplication :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, - resume_url = CASE WHEN $5::boolean THEN $6 ELSE resume_url END -WHERE - user_id = $7 AND event_id = $8 -` - -type UpdateApplicationParams struct { - StatusDoUpdate bool `json:"status_do_update"` - Status ApplicationStatus `json:"status"` - ApplicationDoUpdate bool `json:"application_do_update"` - Application []byte `json:"application"` - ResumeUrlDoUpdate bool `json:"resume_url_do_update"` - ResumeUrl *string `json:"resume_url"` - UserID uuid.UUID `json:"user_id"` - EventID uuid.UUID `json:"event_id"` -} - -func (q *Queries) UpdateApplication(ctx context.Context, arg UpdateApplicationParams) error { - _, err := q.db.Exec(ctx, updateApplication, - arg.StatusDoUpdate, - arg.Status, - arg.ApplicationDoUpdate, - arg.Application, - arg.ResumeUrlDoUpdate, - arg.ResumeUrl, - arg.UserID, - arg.EventID, - ) - return err -} diff --git a/apps/api/internal/db/sqlc/events.sql.go b/apps/api/internal/db/sqlc/events.sql.go deleted file mode 100644 index 25b8de7a..00000000 --- a/apps/api/internal/db/sqlc/events.sql.go +++ /dev/null @@ -1,165 +0,0 @@ -// Code generated by sqlc. DO NOT EDIT. -// versions: -// sqlc v1.29.0 -// source: events.sql - -package sqlc - -import ( - "context" - "time" - - "github.com/google/uuid" -) - -const createEvent = `-- name: CreateEvent :one -INSERT INTO events ( - name, - application_open, application_close, - start_time, end_time -) VALUES ( - $1, - $2, $3, - $4, $5 -) -RETURNING id, name, description, location, location_url, max_attendees, application_open, application_close, rsvp_deadline, decision_release, start_time, end_time, website_url, is_published, saved_at, created_at, updated_at -` - -type CreateEventParams struct { - Name string `json:"name"` - ApplicationOpen time.Time `json:"application_open"` - ApplicationClose time.Time `json:"application_close"` - StartTime time.Time `json:"start_time"` - EndTime time.Time `json:"end_time"` -} - -func (q *Queries) CreateEvent(ctx context.Context, arg CreateEventParams) (Event, error) { - row := q.db.QueryRow(ctx, createEvent, - arg.Name, - arg.ApplicationOpen, - arg.ApplicationClose, - arg.StartTime, - arg.EndTime, - ) - var i Event - err := row.Scan( - &i.ID, - &i.Name, - &i.Description, - &i.Location, - &i.LocationUrl, - &i.MaxAttendees, - &i.ApplicationOpen, - &i.ApplicationClose, - &i.RsvpDeadline, - &i.DecisionRelease, - &i.StartTime, - &i.EndTime, - &i.WebsiteUrl, - &i.IsPublished, - &i.SavedAt, - &i.CreatedAt, - &i.UpdatedAt, - ) - return i, err -} - -const deleteEvent = `-- name: DeleteEvent :exec -DELETE FROM events -WHERE id = $1 -` - -func (q *Queries) DeleteEvent(ctx context.Context, id uuid.UUID) error { - _, err := q.db.Exec(ctx, deleteEvent, id) - return err -} - -const getEventByID = `-- name: GetEventByID :one -SELECT id, name, description, location, location_url, max_attendees, application_open, application_close, rsvp_deadline, decision_release, start_time, end_time, website_url, is_published, saved_at, created_at, updated_at FROM events -WHERE id = $1 -` - -func (q *Queries) GetEventByID(ctx context.Context, id uuid.UUID) (Event, error) { - row := q.db.QueryRow(ctx, getEventByID, id) - var i Event - err := row.Scan( - &i.ID, - &i.Name, - &i.Description, - &i.Location, - &i.LocationUrl, - &i.MaxAttendees, - &i.ApplicationOpen, - &i.ApplicationClose, - &i.RsvpDeadline, - &i.DecisionRelease, - &i.StartTime, - &i.EndTime, - &i.WebsiteUrl, - &i.IsPublished, - &i.SavedAt, - &i.CreatedAt, - &i.UpdatedAt, - ) - return i, err -} - -const updateEventById = `-- name: UpdateEventById :exec -UPDATE events -SET - name = coalesce($1, name), - description = coalesce($2, description), - location = coalesce($3, location), - location_url = coalesce($4, location_url), - max_attendees = coalesce($5, max_attendees), - application_open = coalesce($6, application_open), - application_close = coalesce($7, application_close), - rsvp_deadline = coalesce($8, rsvp_deadline), - decision_release = coalesce($9, decision_release), - start_time = coalesce($10, start_time), - end_time = coalesce($11, end_time), - website_url = coalesce($12, website_url), - is_published = coalesce($13, is_published), - saved_at = coalesce($14, is_published) -WHERE - id = $15::uuid -` - -type UpdateEventByIdParams struct { - Name *string `json:"name"` - Description *string `json:"description"` - Location *string `json:"location"` - LocationUrl *string `json:"location_url"` - MaxAttendees *int32 `json:"max_attendees"` - ApplicationOpen *time.Time `json:"application_open"` - ApplicationClose *time.Time `json:"application_close"` - RsvpDeadline *time.Time `json:"rsvp_deadline"` - DecisionRelease *time.Time `json:"decision_release"` - StartTime *time.Time `json:"start_time"` - EndTime *time.Time `json:"end_time"` - WebsiteUrl *string `json:"website_url"` - IsPublished *bool `json:"is_published"` - SavedAt *time.Time `json:"saved_at"` - ID uuid.UUID `json:"id"` -} - -func (q *Queries) UpdateEventById(ctx context.Context, arg UpdateEventByIdParams) error { - _, err := q.db.Exec(ctx, updateEventById, - arg.Name, - arg.Description, - arg.Location, - arg.LocationUrl, - arg.MaxAttendees, - arg.ApplicationOpen, - arg.ApplicationClose, - arg.RsvpDeadline, - arg.DecisionRelease, - arg.StartTime, - arg.EndTime, - arg.WebsiteUrl, - arg.IsPublished, - arg.SavedAt, - arg.ID, - ) - return err -} diff --git a/apps/api/internal/db/sqlc/models.go b/apps/api/internal/db/sqlc/models.go index 2ca6d713..159ac243 100644 --- a/apps/api/internal/db/sqlc/models.go +++ b/apps/api/internal/db/sqlc/models.go @@ -210,7 +210,6 @@ type Event struct { EndTime time.Time `json:"end_time"` WebsiteUrl *string `json:"website_url"` IsPublished *bool `json:"is_published"` - SavedAt *time.Time `json:"saved_at"` CreatedAt *time.Time `json:"created_at"` UpdatedAt *time.Time `json:"updated_at"` } diff --git a/apps/api/internal/db/sqlc/querier.go b/apps/api/internal/db/sqlc/querier.go index 65bbba66..f07c2601 100644 --- a/apps/api/internal/db/sqlc/querier.go +++ b/apps/api/internal/db/sqlc/querier.go @@ -16,28 +16,20 @@ type Querier interface { // Returns the newly created email record. AddEmail(ctx context.Context, arg AddEmailParams) (EventInterestSubmission, error) CreateAccount(ctx context.Context, arg CreateAccountParams) (AuthAccount, error) - CreateApplication(ctx context.Context, arg CreateApplicationParams) (Application, error) - CreateEvent(ctx context.Context, arg CreateEventParams) (Event, error) CreateSession(ctx context.Context, arg CreateSessionParams) (AuthSession, error) CreateUser(ctx context.Context, arg CreateUserParams) (AuthUser, error) DeleteAccount(ctx context.Context, arg DeleteAccountParams) error - DeleteApplication(ctx context.Context, arg DeleteApplicationParams) error - DeleteEvent(ctx context.Context, id uuid.UUID) error DeleteExpiredSession(ctx context.Context) error DeleteUser(ctx context.Context, id uuid.UUID) error GetActiveSessionUserInfo(ctx context.Context, id uuid.UUID) (GetActiveSessionUserInfoRow, error) - GetApplicationByUserAndEventID(ctx context.Context, arg GetApplicationByUserAndEventIDParams) (Application, error) GetByProviderAndAccountID(ctx context.Context, arg GetByProviderAndAccountIDParams) (AuthAccount, error) GetByUserID(ctx context.Context, userID uuid.UUID) ([]AuthAccount, error) - GetEventByID(ctx context.Context, id uuid.UUID) (Event, error) GetSessionByID(ctx context.Context, id uuid.UUID) (AuthSession, error) GetSessionsByUserID(ctx context.Context, userID uuid.UUID) ([]AuthSession, error) GetUserByEmail(ctx context.Context, email *string) (AuthUser, error) GetUserByID(ctx context.Context, id uuid.UUID) (AuthUser, error) InvalidateSessionByID(ctx context.Context, id uuid.UUID) error TouchSession(ctx context.Context, arg TouchSessionParams) error - UpdateApplication(ctx context.Context, arg UpdateApplicationParams) error - UpdateEventById(ctx context.Context, arg UpdateEventByIdParams) error UpdateSessionExpiration(ctx context.Context, arg UpdateSessionExpirationParams) error UpdateTokens(ctx context.Context, arg UpdateTokensParams) error UpdateUser(ctx context.Context, arg UpdateUserParams) error diff --git a/apps/docs/mkdocs.yml b/apps/docs/mkdocs.yml index 40af77ab..21a8abd8 100644 --- a/apps/docs/mkdocs.yml +++ b/apps/docs/mkdocs.yml @@ -50,7 +50,6 @@ nav: - Overview: api/index.md - Installation: api/installation.md - OpenAPI: 'https://core.apidocumentation.com/guide/swamphacks-core-api' - - Database testing: 'api/db_testing.md' - Discord Bot: - Overview: discord-bot/index.md - Installation: discord-bot/installation.md diff --git a/apps/docs/src/api/db_testing.md b/apps/docs/src/api/db_testing.md deleted file mode 100644 index 6a0d5f19..00000000 --- a/apps/docs/src/api/db_testing.md +++ /dev/null @@ -1,30 +0,0 @@ -# Database testing - -## Manual - -1. Make sure the docker instance is currently running (i.e. you ran `$ docker compose up`) -!!! note - - You may have to run docker with sudo depending on your system configuration. - -1. In a seperate terminal (I suggest you try using tmux) list the current docker processes -``` bash -docker ps -``` - -1. Copy the process id for the docker container running postgres, and paste into the following command in order to spawn a shell with access to the container. -``` bash -sudo docker exec -it ef7XXXXXX07e sh -``` - -1. Connect to the postgres database using a database url. The url can be found inside `core/apps/api/.env.example`. -``` bash -psql postgres://postgres:postgres@postgres:5432/coredb -``` - -1. Check to see if database tables currently exist. You can by listing the currently created tables. -``` bash -\dt -``` -If there is nothing here, then go into `/core/apps/api` and run `make migrate`, which runs sql commands added to [migrations](https://en.wikipedia.org/wiki/Schema_migration) in `core/apps/api/internal/db/migrations`. -1. You can now test to see if rows, columns and tables are updated appropriately with psql commands. Use a [reference to psql](https://www.postgresql.org/docs/17/app-psql.html) if you need help finding commands. diff --git a/apps/docs/src/api/installation.md b/apps/docs/src/api/installation.md index d6840531..0283f2ea 100644 --- a/apps/docs/src/api/installation.md +++ b/apps/docs/src/api/installation.md @@ -3,25 +3,23 @@ ### Setup with Docker Compose (main setup) 1. Navigate to `core/apps/api` -1. **Set up environment variables**: +2. **Set up environment variables**: ``` bash cp .env.dev.example .env.dev ``` -1. Open `.env.dev` +3. Open `.env.dev` 1. For `AUTH_DISCORD_CLIENT_ID` and `AUTH_DISCORD_CLIENT_SECRET`, go to the Discord developer portal and create an account. Create a new application and go to the OAuth2 tab in the left sidebar. Copy the Client ID and the Client Secret into their respective environment variables. - 1. While in the OAuth2 menu, copy the `AUTH_DISCORD_REDIRECT_URI` parameter from the example configuration and paste into the box under the *Redirects* header. This is the URL which discord will redirect the user to after Discord authentication has completed. + 2. Fill out any other required keys and tokens, if empty. - 1. Fill out any other required keys and tokens, if empty. - -1. Continue with the [main setup instructions](../getting-started.md) +3. Continue with the [main setup instructions](../getting-started.md) ### Setup without Docker Compose 1. Make sure you have [Go](https://go.dev/) installed on your system. -1. Initialize the Go project +2. Initialize the Go project ``` bash go mod tidy ``` @@ -31,7 +29,7 @@ go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest go install github.com/pressly/goose/v3/cmd/goose@latest ``` -1. Run the program with +3. Run the program with ```bash air ``` diff --git a/docker-compose.yml b/docker-compose.yml index d7bb5bc8..292d7adb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,7 +17,6 @@ services: dockerfile: Dockerfile.dev volumes: - ./apps/web:/app - - web_node_modules:/app/node_modules ports: - "5173:5173" environment: @@ -47,6 +46,5 @@ services: volumes: postgres_data: - web_node_modules: