From e627752181fc92c76192676c195183cb5a75e1b6 Mon Sep 17 00:00:00 2001 From: VishakhaSainani-Josh Date: Mon, 21 Jul 2025 15:00:09 +0530 Subject: [PATCH 01/36] improve folder structure --- backend/README.md | 53 +++ {cmd => backend/cmd}/main.go | 19 +- backend/go.mod | 65 ++++ backend/go.sum | 213 ++++++++++++ .../internal}/app/auth/domain.go | 0 .../internal}/app/auth/handler.go | 0 .../internal}/app/auth/service.go | 8 + backend/internal/app/badge/domain.go | 12 + backend/internal/app/badge/handler.go | 48 +++ backend/internal/app/badge/service.go | 59 ++++ backend/internal/app/bigquery/domain.go | 44 +++ backend/internal/app/bigquery/service.go | 53 +++ backend/internal/app/contribution/domain.go | 70 ++++ backend/internal/app/contribution/handler.go | 81 +++++ backend/internal/app/contribution/service.go | 307 ++++++++++++++++++ backend/internal/app/cronJob/cleanupJob.go | 32 ++ backend/internal/app/cronJob/cronjob.go | 24 ++ backend/internal/app/cronJob/dailyJob.go | 32 ++ backend/internal/app/cronJob/init.go | 40 +++ backend/internal/app/dependencies.go | 71 ++++ backend/internal/app/github/domain.go | 30 ++ backend/internal/app/github/service.go | 93 ++++++ backend/internal/app/goal/domain.go | 26 ++ backend/internal/app/goal/handler.go | 117 +++++++ backend/internal/app/goal/service.go | 164 ++++++++++ backend/internal/app/repository/domain.go | 56 ++++ backend/internal/app/repository/handler.go | 161 +++++++++ backend/internal/app/repository/service.go | 167 ++++++++++ backend/internal/app/router.go | 44 +++ backend/internal/app/transaction/domain.go | 28 ++ backend/internal/app/transaction/service.go | 109 +++++++ .../internal}/app/user/domain.go | 28 +- backend/internal/app/user/handler.go | 144 ++++++++ backend/internal/app/user/service.go | 182 +++++++++++ {internal => backend/internal}/config/app.go | 18 +- backend/internal/config/bigquery.go | 23 ++ {internal => backend/internal}/config/db.go | 0 {internal => backend/internal}/db/migrate.go | 18 +- .../db/migrations/1748862201_init.down.sql | 0 .../db/migrations/1748862201_init.up.sql | 14 +- ...28591_add_column_contributors_url.down.sql | 1 + ...0328591_add_column_contributors_url.up.sql | 1 + ...016438_allow-null-contribution-id.down.sql | 2 + ...51016438_allow-null-contribution-id.up.sql | 2 + .../1751028730_add-gh-event-id.down.sql | 1 + .../1751028730_add-gh-event-id.up.sql | 1 + ...661_set-not-null-contributors-url.down.sql | 1 + ...66661_set-not-null-contributors-url.up.sql | 1 + ...51268286_set-not-null-gh-event-id.down.sql | 1 + ...1751268286_set-not-null-gh-event-id.up.sql | 1 + ...reate-index-users-current-balance.down.sql | 1 + ..._create-index-users-current-balance.up.sql | 1 + backend/internal/pkg/apperrors/errors.go | 77 +++++ {internal => backend/internal}/pkg/jwt/jwt.go | 0 .../internal}/pkg/middleware/middleware.go | 14 + .../internal}/pkg/response/response.go | 0 backend/internal/pkg/utils/helper.go | 79 +++++ backend/internal/repository/badge.go | 86 +++++ .../internal}/repository/base.go | 5 +- backend/internal/repository/contribution.go | 187 +++++++++++ backend/internal/repository/domain.go | 121 +++++++ backend/internal/repository/goal.go | 139 ++++++++ backend/internal/repository/repository.go | 180 ++++++++++ backend/internal/repository/transaction.go | 79 +++++ backend/internal/repository/user.go | 260 +++++++++++++++ go.mod | 25 -- go.sum | 52 --- internal/app/dependencies.go | 35 -- internal/app/router.go | 24 -- internal/app/user/handler.go | 46 --- internal/app/user/service.go | 76 ----- internal/pkg/apperrors/errors.go | 50 --- internal/repository/domain.go | 31 -- internal/repository/user.go | 158 --------- 74 files changed, 3867 insertions(+), 524 deletions(-) create mode 100644 backend/README.md rename {cmd => backend/cmd}/main.go (73%) create mode 100644 backend/go.mod create mode 100644 backend/go.sum rename {internal => backend/internal}/app/auth/domain.go (100%) rename {internal => backend/internal}/app/auth/handler.go (100%) rename {internal => backend/internal}/app/auth/service.go (92%) create mode 100644 backend/internal/app/badge/domain.go create mode 100644 backend/internal/app/badge/handler.go create mode 100644 backend/internal/app/badge/service.go create mode 100644 backend/internal/app/bigquery/domain.go create mode 100644 backend/internal/app/bigquery/service.go create mode 100644 backend/internal/app/contribution/domain.go create mode 100644 backend/internal/app/contribution/handler.go create mode 100644 backend/internal/app/contribution/service.go create mode 100644 backend/internal/app/cronJob/cleanupJob.go create mode 100644 backend/internal/app/cronJob/cronjob.go create mode 100644 backend/internal/app/cronJob/dailyJob.go create mode 100644 backend/internal/app/cronJob/init.go create mode 100644 backend/internal/app/dependencies.go create mode 100644 backend/internal/app/github/domain.go create mode 100644 backend/internal/app/github/service.go create mode 100644 backend/internal/app/goal/domain.go create mode 100644 backend/internal/app/goal/handler.go create mode 100644 backend/internal/app/goal/service.go create mode 100644 backend/internal/app/repository/domain.go create mode 100644 backend/internal/app/repository/handler.go create mode 100644 backend/internal/app/repository/service.go create mode 100644 backend/internal/app/router.go create mode 100644 backend/internal/app/transaction/domain.go create mode 100644 backend/internal/app/transaction/service.go rename {internal => backend/internal}/app/user/domain.go (64%) create mode 100644 backend/internal/app/user/handler.go create mode 100644 backend/internal/app/user/service.go rename {internal => backend/internal}/config/app.go (65%) create mode 100644 backend/internal/config/bigquery.go rename {internal => backend/internal}/config/db.go (100%) rename {internal => backend/internal}/db/migrate.go (94%) rename {internal => backend/internal}/db/migrations/1748862201_init.down.sql (100%) rename {internal => backend/internal}/db/migrations/1748862201_init.up.sql (92%) create mode 100644 backend/internal/db/migrations/1750328591_add_column_contributors_url.down.sql create mode 100644 backend/internal/db/migrations/1750328591_add_column_contributors_url.up.sql create mode 100644 backend/internal/db/migrations/1751016438_allow-null-contribution-id.down.sql create mode 100644 backend/internal/db/migrations/1751016438_allow-null-contribution-id.up.sql create mode 100644 backend/internal/db/migrations/1751028730_add-gh-event-id.down.sql create mode 100644 backend/internal/db/migrations/1751028730_add-gh-event-id.up.sql create mode 100644 backend/internal/db/migrations/1751266661_set-not-null-contributors-url.down.sql create mode 100644 backend/internal/db/migrations/1751266661_set-not-null-contributors-url.up.sql create mode 100644 backend/internal/db/migrations/1751268286_set-not-null-gh-event-id.down.sql create mode 100644 backend/internal/db/migrations/1751268286_set-not-null-gh-event-id.up.sql create mode 100644 backend/internal/db/migrations/1752476063_create-index-users-current-balance.down.sql create mode 100644 backend/internal/db/migrations/1752476063_create-index-users-current-balance.up.sql create mode 100644 backend/internal/pkg/apperrors/errors.go rename {internal => backend/internal}/pkg/jwt/jwt.go (100%) rename {internal => backend/internal}/pkg/middleware/middleware.go (84%) rename {internal => backend/internal}/pkg/response/response.go (100%) create mode 100644 backend/internal/pkg/utils/helper.go create mode 100644 backend/internal/repository/badge.go rename {internal => backend/internal}/repository/base.go (88%) create mode 100644 backend/internal/repository/contribution.go create mode 100644 backend/internal/repository/domain.go create mode 100644 backend/internal/repository/goal.go create mode 100644 backend/internal/repository/repository.go create mode 100644 backend/internal/repository/transaction.go create mode 100644 backend/internal/repository/user.go delete mode 100644 go.mod delete mode 100644 go.sum delete mode 100644 internal/app/dependencies.go delete mode 100644 internal/app/router.go delete mode 100644 internal/app/user/handler.go delete mode 100644 internal/app/user/service.go delete mode 100644 internal/pkg/apperrors/errors.go delete mode 100644 internal/repository/domain.go delete mode 100644 internal/repository/user.go diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 00000000..3b2eabfc --- /dev/null +++ b/backend/README.md @@ -0,0 +1,53 @@ +What can be measured, can be improved! + +Everyone knows that running is a great exercise. However, by using a wearable like the Fitbit™ to measure your activity, it has motivated people to run regularly. + +CodeCuriosity aims to be the Fitbit™ for open-source contributions +Code Curiosity is a platform to encourage open source contribution, build a cohesive and collaborative open source community and reward open source contributors across the world. It measures open-source contributions (code commits, issues and comments) and gives points that can be redeemed against Github or Amazon Gift Cards! + +CodeCuriosity aims to reward everyone (experts or newbies) and everything (minor changes to features). + +Out of the 30M+ open-source developers on Github, if we can motivate even a fraction to contribute more, we are sure we can make the open-soure community more vibrant and rewarding! We reward open-source contributions from our own pocket, so "The more money we lose, the more the open-source community gains." + +Are you or your company interested in supporting CodeCuriosity? +If you or your company wants to support this cause, please contact us at info@codecuriosity.org. + +You do not have to donate any money! We would expect you to pledge an amount in USD and fulfill redemption requests from registered CodeCuriosity users. Here is the list of companies that are supporting this cause: + +Josh Software has pledged 1000 USD. +You or your company? +What is CodeCuriosity all about? +Here are some questions that should help you get going. A lot more questions that are answered in the FAQ. + +Who should use CodeCuriosity? +Everyone who contributes to open source. It's not only fun but you're also making a difference! Since all commits are rewarded and everyone (experts and newbies alike) is rewarded, it's always encouraging. There are so many repository owners that are looking out for help from developers and CodeCuriosity not only helps you help others but also rewards you while doing it. It's about having your cake and eating it too!. + +How does it work? +We gamify open source contributions. + +Sign-up with your GitHub account on CodeCuriosity. After that, we do the rest! +The system analyses your commits and activities and they are automatically scored and shown on your dashboard. +Choose your monthly goal and try to achieve it. +Every month-end, the cumulative scores are added to your wallet and if you have achieved your goal, you get bonus points! +What are points and how do they work? +Points are the scores you have accumulated for your contributions. You can redeem these points for Github or Amazon Gift cards. We also support other stores and if you send us details, we shall ensure you get rewarded! + +New to Open Source and don't know where to start? +Don't worry! Start with Code Triage. It's a very good way to choose which language and repositories you want to contribute to. It sends you some GitHub issues that you can look at everyday! + +Still too complex for you? Start by reading and contributing back some documentation at DocsDoctor. + +Contributing to CodeCuriosity +If you have some cool ideas, comments or feedback - you can mail us at info@codecuriosity.org. + +We don't have a Slack / IRC channel or mailing list yet -- we shall work on it soon. + +To contribute to code, fork this repository, raise issues, suggest features and help us make the change! + +To test this locally, + +create .env. file based on your environment i.e .env.development, .env.production. For local development, use .env.local +Add github app id and secret to .env. file. Refer env.sample +About +Open-source is now fun and rewarding! + diff --git a/cmd/main.go b/backend/cmd/main.go similarity index 73% rename from cmd/main.go rename to backend/cmd/main.go index 35ae97a7..c0fc99e0 100644 --- a/cmd/main.go +++ b/backend/cmd/main.go @@ -6,25 +6,25 @@ import ( "log/slog" "net/http" "os" - + "os/signal" "syscall" "time" "github.com/joshsoftware/code-curiosity-2025/internal/app" + "github.com/joshsoftware/code-curiosity-2025/internal/app/cronJob" "github.com/joshsoftware/code-curiosity-2025/internal/config" ) func main() { ctx := context.Background() - cfg,err := config.LoadAppConfig() + cfg, err := config.LoadAppConfig() if err != nil { slog.Error("error loading app config", "error", err) return } - db, err := config.InitDataStore(cfg) if err != nil { slog.Error("error initializing database", "error", err) @@ -32,10 +32,21 @@ func main() { } defer db.Close() - dependencies := app.InitDependencies(db,cfg) + bigqueryInstance, err := config.BigqueryInit(ctx, cfg) + if err != nil { + slog.Error("error initializing bigquery", "error", err) + return + } + + httpClient := &http.Client{} + + dependencies := app.InitDependencies(db, cfg, bigqueryInstance, httpClient) router := app.NewRouter(dependencies) + newCronSchedular := cronJob.NewCronSchedular() + newCronSchedular.InitCronJobs(dependencies.ContributionService, dependencies.UserService) + server := http.Server{ Addr: fmt.Sprintf(":%s", cfg.HTTPServer.Port), Handler: router, diff --git a/backend/go.mod b/backend/go.mod new file mode 100644 index 00000000..e73f2e26 --- /dev/null +++ b/backend/go.mod @@ -0,0 +1,65 @@ +module github.com/joshsoftware/code-curiosity-2025 + +go 1.23.4 + +require ( + cloud.google.com/go/bigquery v1.68.0 + github.com/golang-jwt/jwt/v4 v4.5.2 + github.com/golang-migrate/migrate/v4 v4.18.3 + github.com/ilyakaznacheev/cleanenv v1.5.0 + github.com/jmoiron/sqlx v1.4.0 + github.com/lib/pq v1.10.9 + github.com/robfig/cron/v3 v3.0.1 + golang.org/x/oauth2 v0.29.0 + google.golang.org/api v0.231.0 +) + +require ( + cloud.google.com/go v0.121.0 // indirect + cloud.google.com/go/auth v0.16.1 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect + cloud.google.com/go/compute/metadata v0.6.0 // indirect + cloud.google.com/go/iam v1.5.2 // indirect + github.com/BurntSushi/toml v1.2.1 // indirect + github.com/apache/arrow/go/v15 v15.0.2 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/google/flatbuffers v23.5.26+incompatible // indirect + github.com/google/s2a-go v0.1.9 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect + github.com/googleapis/gax-go/v2 v2.14.1 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/joho/godotenv v1.5.1 // indirect + github.com/klauspost/compress v1.16.7 // indirect + github.com/klauspost/cpuid/v2 v2.2.5 // indirect + github.com/pierrec/lz4/v4 v4.1.18 // indirect + github.com/zeebo/xxh3 v1.0.2 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect + go.uber.org/atomic v1.11.0 // indirect + golang.org/x/crypto v0.37.0 // indirect + golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect + golang.org/x/mod v0.23.0 // indirect + golang.org/x/net v0.39.0 // indirect + golang.org/x/sync v0.14.0 // indirect + golang.org/x/sys v0.32.0 // indirect + golang.org/x/text v0.24.0 // indirect + golang.org/x/time v0.11.0 // indirect + golang.org/x/tools v0.30.0 // indirect + golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect + google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 // indirect + google.golang.org/grpc v1.72.0 // indirect + google.golang.org/protobuf v1.36.6 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 // indirect +) diff --git a/backend/go.sum b/backend/go.sum new file mode 100644 index 00000000..a5992483 --- /dev/null +++ b/backend/go.sum @@ -0,0 +1,213 @@ +cel.dev/expr v0.20.0 h1:OunBvVCfvpWlt4dN7zg3FM6TDkzOePe1+foGJ9AXeeI= +cel.dev/expr v0.20.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= +cloud.google.com/go v0.121.0 h1:pgfwva8nGw7vivjZiRfrmglGWiCJBP+0OmDpenG/Fwg= +cloud.google.com/go v0.121.0/go.mod h1:rS7Kytwheu/y9buoDmu5EIpMMCI4Mb8ND4aeN4Vwj7Q= +cloud.google.com/go/auth v0.16.1 h1:XrXauHMd30LhQYVRHLGvJiYeczweKQXZxsTbV9TiguU= +cloud.google.com/go/auth v0.16.1/go.mod h1:1howDHJ5IETh/LwYs3ZxvlkXF48aSqqJUM+5o02dNOI= +cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= +cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= +cloud.google.com/go/bigquery v1.68.0 h1:F+CPqdcMxZGUDBACzGtOJ1E6E0MWSYcKeFthxnhpYIU= +cloud.google.com/go/bigquery v1.68.0/go.mod h1:1UAksG8IFXJomQV38xUsRB+2m2c1H9U0etvoGHgyhDk= +cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= +cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= +cloud.google.com/go/datacatalog v1.26.0 h1:eFgygb3DTufTWWUB8ARk+dSuXz+aefNJXTlkWlQcWwE= +cloud.google.com/go/datacatalog v1.26.0/go.mod h1:bLN2HLBAwB3kLTFT5ZKLHVPj/weNz6bR0c7nYp0LE14= +cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8= +cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE= +cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE= +cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY= +cloud.google.com/go/monitoring v1.24.0 h1:csSKiCJ+WVRgNkRzzz3BPoGjFhjPY23ZTcaenToJxMM= +cloud.google.com/go/monitoring v1.24.0/go.mod h1:Bd1PRK5bmQBQNnuGwHBfUamAV1ys9049oEPHnn4pcsc= +cloud.google.com/go/storage v1.53.0 h1:gg0ERZwL17pJ+Cz3cD2qS60w1WMDnwcm5YPAIQBHUAw= +cloud.google.com/go/storage v1.53.0/go.mod h1:7/eO2a/srr9ImZW9k5uufcNahT2+fPb8w5it1i5boaA= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= +github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 h1:ErKg/3iS1AKcTkf3yixlZ54f9U1rljCkQyEXWUnIUxc= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0/go.mod h1:yAZHSGnqScoU556rBOVkwLze6WP5N+U11RHuWaGVxwY= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 h1:fYE9p3esPxA/C0rQ0AHhP0drtPXDRhaWiwg1DPqO7IU= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0/go.mod h1:BnBReJLvVYx2CS/UHOgVz2BXKXD9wsQPxZug20nZhd0= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 h1:6/0iUd0xrnX7qt+mLNRwg5c0PGv8wpE8K90ryANQwMI= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0/go.mod h1:otE2jQekW/PqXk1Awf5lmfokJx4uwuqcj1ab5SpGeW0= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/apache/arrow/go/v15 v15.0.2 h1:60IliRbiyTWCWjERBCkO1W4Qun9svcYoZrSLcyOsMLE= +github.com/apache/arrow/go/v15 v15.0.2/go.mod h1:DGXsR3ajT524njufqf95822i+KTh+yea1jass9YXgjA= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42 h1:Om6kYQYDUk5wWbT0t0q6pvyM49i9XZAv9dDrkDA7gjk= +github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +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/dhui/dktest v0.4.5 h1:uUfYBIVREmj/Rw6MvgmqNAYzTiKOHJak+enB5Di73MM= +github.com/dhui/dktest v0.4.5/go.mod h1:tmcyeHDKagvlDrz7gDKq4UAJOLIfVZYkfD5OnHDwcCo= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v27.2.0+incompatible h1:Rk9nIVdfH3+Vz4cyI/uhbINhEZ/oLmc+CBXmH6fbNk4= +github.com/docker/docker v27.2.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M= +github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A= +github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw= +github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= +github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E= +github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-migrate/migrate/v4 v4.18.3 h1:EYGkoOsvgHHfm5U/naS1RP/6PL/Xv3S4B/swMiAmDLs= +github.com/golang-migrate/migrate/v4 v4.18.3/go.mod h1:99BKpIi6ruaaXRM1A77eqZ+FWPQ3cfRa+ZVy5bmWMaY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/flatbuffers v23.5.26+incompatible h1:M9dgRyhJemaM4Sw8+66GHBu8ioaQmyPLg1b8VwK5WJg= +github.com/google/flatbuffers v23.5.26+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc= +github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0= +github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= +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/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= +github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= +github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q= +github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/ilyakaznacheev/cleanenv v1.5.0 h1:0VNZXggJE2OYdXE87bfSSwGxeiGt9moSR2lOrsHHvr4= +github.com/ilyakaznacheev/cleanenv v1.5.0/go.mod h1:a5aDzaJrLCQZsazHol1w8InnDcOX0OColm64SlIi6gk= +github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= +github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= +github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= +github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +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-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ= +github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= +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/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE= +github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= +github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= +github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM= +github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= +github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= +github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/detectors/gcp v1.35.0 h1:bGvFt68+KTiAKFlacHW6AhA56GF2rS0bdD3aJYEnmzA= +go.opentelemetry.io/contrib/detectors/gcp v1.35.0/go.mod h1:qGWP8/+ILwMRIUf9uIVLloR1uo5ZYAslM4O6OqUi1DA= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= +go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= +go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= +go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= +golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= +golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= +golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= +golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= +golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98= +golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +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.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= +golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= +golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= +golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= +gonum.org/v1/gonum v0.12.0 h1:xKuo6hzt+gMav00meVPUlXwSdoEJP46BR+wdxQEFK2o= +gonum.org/v1/gonum v0.12.0/go.mod h1:73TDxJfAAHeA8Mk9mf8NlIppyhQNo5GLTcYeqgo2lvY= +google.golang.org/api v0.231.0 h1:LbUD5FUl0C4qwia2bjXhCMH65yz1MLPzA/0OYEsYY7Q= +google.golang.org/api v0.231.0/go.mod h1:H52180fPI/QQlUc0F4xWfGZILdv09GCWKt2bcsn164A= +google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb h1:ITgPrl429bc6+2ZraNSzMDk3I95nmQln2fuPstKwFDE= +google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:sAo5UzpjUwgFBCzupwhcLcxHVDK7vG5IqI30YnwX2eE= +google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34 h1:0PeQib/pH3nB/5pEmFeVQJotzGohV0dq4Vcp09H5yhE= +google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34/go.mod h1:0awUlEkap+Pb1UMeJwJQQAdJQrt3moU7J2moTy69irI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 h1:h6p3mQqrmT1XkHVTfzLdNz1u7IhINeZkz67/xTbOuWs= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM= +google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 h1:slmdOY3vp8a7KQbHkL+FLbvbkgMqmXojpFUO/jENuqQ= +olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3/go.mod h1:oVgVk4OWVDi43qWBEyGhXgYxt7+ED4iYNpTngSLX2Iw= diff --git a/internal/app/auth/domain.go b/backend/internal/app/auth/domain.go similarity index 100% rename from internal/app/auth/domain.go rename to backend/internal/app/auth/domain.go diff --git a/internal/app/auth/handler.go b/backend/internal/app/auth/handler.go similarity index 100% rename from internal/app/auth/handler.go rename to backend/internal/app/auth/handler.go diff --git a/internal/app/auth/service.go b/backend/internal/app/auth/service.go similarity index 92% rename from internal/app/auth/service.go rename to backend/internal/app/auth/service.go index 3bca6c0d..c0cbeb4f 100644 --- a/internal/app/auth/service.go +++ b/backend/internal/app/auth/service.go @@ -83,6 +83,14 @@ func (s *service) GithubOAuthLoginCallback(ctx context.Context, code string) (st return "", apperrors.ErrInternalServer } + if userData.IsDeleted { + err = s.userService.RecoverAccountInGracePeriod(ctx, userData.Id) + if err != nil { + slog.Error("error in recovering account in grace period during login", "error", err) + return "", apperrors.ErrInternalServer + } + } + return jwtToken, nil } diff --git a/backend/internal/app/badge/domain.go b/backend/internal/app/badge/domain.go new file mode 100644 index 00000000..d2c97977 --- /dev/null +++ b/backend/internal/app/badge/domain.go @@ -0,0 +1,12 @@ +package badge + +import "time" + +type Badge struct { + Id int + UserId int + BadgeType string + EarnedAt time.Time + CreatedAt time.Time + UpdatedAt time.Time +} diff --git a/backend/internal/app/badge/handler.go b/backend/internal/app/badge/handler.go new file mode 100644 index 00000000..0cd456dd --- /dev/null +++ b/backend/internal/app/badge/handler.go @@ -0,0 +1,48 @@ +package badge + +import ( + "log/slog" + "net/http" + + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/middleware" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/response" +) + +type handler struct { + badgeService Service +} + +type Handler interface { + GetBadgeDetailsOfUser(w http.ResponseWriter, r *http.Request) +} + +func NewHandler(badgeService Service) Handler { + return &handler{ + badgeService: badgeService, + } +} + +func (h *handler) GetBadgeDetailsOfUser(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + userIdValue := ctx.Value(middleware.UserIdKey) + userId, ok := userIdValue.(int) + if !ok { + slog.Error("error obtaining user id from context") + status, errorMsg := apperrors.MapError(apperrors.ErrContextValue) + response.WriteJson(w, status, errorMsg, nil) + return + } + + badges, err := h.badgeService.GetBadgeDetailsOfUser(ctx, userId) + + if err != nil { + slog.Error("failed to get badge details of user", "Error", err) + status, errorMsg := apperrors.MapError(err) + response.WriteJson(w, status, errorMsg, nil) + return + } + + response.WriteJson(w, http.StatusOK, "badges fetched successfully", badges) +} diff --git a/backend/internal/app/badge/service.go b/backend/internal/app/badge/service.go new file mode 100644 index 00000000..817ca283 --- /dev/null +++ b/backend/internal/app/badge/service.go @@ -0,0 +1,59 @@ +package badge + +import ( + "context" + "database/sql" + "errors" + "log/slog" + + "github.com/joshsoftware/code-curiosity-2025/internal/repository" +) + +type service struct { + badgeRepository repository.BadgeRepository +} + +type Service interface { + HandleBadgeCreation(ctx context.Context, userId int, badgeType string) (Badge, error) + GetBadgeDetailsOfUser(ctx context.Context, userId int) ([]Badge, error) +} + +func NewService(badgeRepository repository.BadgeRepository) Service { + return &service{ + badgeRepository: badgeRepository, + } +} + +func (s *service) HandleBadgeCreation(ctx context.Context, userId int, badgeType string) (Badge, error) { + badge, err := s.badgeRepository.GetUserCurrentMonthBadge(ctx, nil, userId) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + badge, err = s.badgeRepository.CreateBadge(ctx, nil, userId, badgeType) + if err != nil { + slog.Error("error creating badge for user", "error", err) + return Badge{}, err + } + } + slog.Error("error fetching current month badge for user", "error", err) + return Badge{}, err + } + + return Badge(badge), nil +} + +func (s *service) GetBadgeDetailsOfUser(ctx context.Context, userId int) ([]Badge, error) { + badges, err := s.badgeRepository.GetBadgeDetailsOfUser(ctx, nil, userId) + + if err != nil { + slog.Error("(service) Failed to get the badge details", "error", err) + return nil, err + } + + serviceBadge := make([]Badge, len(badges)) + + for i, b := range badges { + serviceBadge[i] = Badge(b) + } + + return serviceBadge, nil +} diff --git a/backend/internal/app/bigquery/domain.go b/backend/internal/app/bigquery/domain.go new file mode 100644 index 00000000..bedd595d --- /dev/null +++ b/backend/internal/app/bigquery/domain.go @@ -0,0 +1,44 @@ +package bigquery + +import "time" + +const DailyQuery = `SELECT + id, + type, + public, + actor.id AS actor_id, + actor.login AS actor_login, + actor.gravatar_id AS actor_gravatar_id, + actor.url AS actor_url, + actor.avatar_url AS actor_avatar_url, + repo.id AS repo_id, + repo.name AS repo_name, + repo.url AS repo_url, + payload, + created_at, + other +FROM + githubarchive.day.%s +WHERE + type IN ( + 'IssuesEvent', + 'PullRequestEvent', + 'PullRequestReviewEvent', + 'IssueCommentEvent', + 'PullRequestReviewCommentEvent' + ) + AND ( + actor.id IN (%s) + )` + +type ContributionResponse struct { + ID string `bigquery:"id"` + Type string `bigquery:"type"` + ActorID int `bigquery:"actor_id"` + ActorLogin string `bigquery:"actor_login"` + RepoID int `bigquery:"repo_id"` + RepoName string `bigquery:"repo_name"` + RepoUrl string `bigquery:"repo_url"` + Payload string `bigquery:"payload"` + CreatedAt time.Time `bigquery:"created_at"` +} diff --git a/backend/internal/app/bigquery/service.go b/backend/internal/app/bigquery/service.go new file mode 100644 index 00000000..21a20644 --- /dev/null +++ b/backend/internal/app/bigquery/service.go @@ -0,0 +1,53 @@ +package bigquery + +import ( + "context" + "fmt" + "log/slog" + "time" + + bq "cloud.google.com/go/bigquery" + "github.com/joshsoftware/code-curiosity-2025/internal/config" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/utils" + "github.com/joshsoftware/code-curiosity-2025/internal/repository" +) + +type service struct { + bigqueryInstance config.Bigquery + userRepository repository.UserRepository +} + +type Service interface { + FetchDailyContributions(ctx context.Context) (*bq.RowIterator, error) +} + +func NewService(bigqueryInstance config.Bigquery, userRepository repository.UserRepository) Service { + return &service{ + bigqueryInstance: bigqueryInstance, + userRepository: userRepository, + } +} + +func (s *service) FetchDailyContributions(ctx context.Context) (*bq.RowIterator, error) { + usersGithubId, err := s.userRepository.GetAllUsersGithubId(ctx, nil) + if err != nil { + slog.Error("error fetching users github usernames") + return nil, apperrors.ErrInternalServer + } + + formattedGithubIds := utils.FormatIntSliceForQuery(usersGithubId) + + YesterdayDate := time.Now().AddDate(0, 0, -1) + YesterdayYearMonthDay := YesterdayDate.Format("20060102") + fetchDailyContributionsQuery := fmt.Sprintf(DailyQuery, YesterdayYearMonthDay, formattedGithubIds) + + bigqueryQuery := s.bigqueryInstance.Client.Query(fetchDailyContributionsQuery) + contributionRows, err := bigqueryQuery.Read(ctx) + if err != nil { + slog.Error("error fetching contributions", "error", err) + return nil, err + } + + return contributionRows, err +} diff --git a/backend/internal/app/contribution/domain.go b/backend/internal/app/contribution/domain.go new file mode 100644 index 00000000..238f5dca --- /dev/null +++ b/backend/internal/app/contribution/domain.go @@ -0,0 +1,70 @@ +package contribution + +import "time" + +type ContributionResponse struct { + ID string `bigquery:"id"` + Type string `bigquery:"type"` + ActorID int `bigquery:"actor_id"` + ActorLogin string `bigquery:"actor_login"` + RepoID int `bigquery:"repo_id"` + RepoName string `bigquery:"repo_name"` + RepoUrl string `bigquery:"repo_url"` + Payload string `bigquery:"payload"` + CreatedAt time.Time `bigquery:"created_at"` +} + +type Repository struct { + Id int + GithubRepoId int + RepoName string + Description string + LanguagesUrl string + RepoUrl string + OwnerName string + UpdateDate time.Time + ContributorsUrl string + CreatedAt time.Time + UpdatedAt time.Time +} + +type Contribution struct { + Id int + UserId int + RepositoryId int + ContributionScoreId int + ContributionType string + BalanceChange int + ContributedAt time.Time + GithubEventId string + CreatedAt time.Time + UpdatedAt time.Time +} + +type ContributionScore struct { + Id int + AdminId int + ContributionType string + Score int + CreatedAt time.Time + UpdatedAt time.Time +} + +type Transaction struct { + Id int + UserId int + ContributionId int + IsRedeemed bool + IsGained bool + TransactedBalance int + TransactedAt time.Time + CreatedAt time.Time + UpdatedAt time.Time +} + +type MonthlyContributionSummary struct { + Type string + Count int + TotalCoins int + Month time.Time +} diff --git a/backend/internal/app/contribution/handler.go b/backend/internal/app/contribution/handler.go new file mode 100644 index 00000000..8d604ad6 --- /dev/null +++ b/backend/internal/app/contribution/handler.go @@ -0,0 +1,81 @@ +package contribution + +import ( + "log/slog" + "net/http" + + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/middleware" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/response" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/utils" +) + +type handler struct { + contributionService Service +} + +type Handler interface { + FetchUserContributions(w http.ResponseWriter, r *http.Request) + ListMonthlyContributionSummary(w http.ResponseWriter, r *http.Request) +} + +func NewHandler(contributionService Service) Handler { + return &handler{ + contributionService: contributionService, + } +} + +func (h *handler) FetchUserContributions(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + userContributions, err := h.contributionService.FetchUserContributions(ctx) + if err != nil { + slog.Error("error fetching user contributions", "error", err) + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) + return + } + + response.WriteJson(w, http.StatusOK, "user contributions fetched successfully", userContributions) +} + +func (h *handler) ListMonthlyContributionSummary(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + userIdValue := ctx.Value(middleware.UserIdKey) + userId, ok := userIdValue.(int) + if !ok { + slog.Error("error obtaining user id from context") + status, errorMessage := apperrors.MapError(apperrors.ErrContextValue) + response.WriteJson(w, status, errorMessage, nil) + return + } + + yearVal := r.URL.Query().Get("year") + year, err := utils.ValidateYearQueryParam(yearVal) + if err != nil { + slog.Error("error converting year value to integer", "error", err) + status, errorMessage := apperrors.MapError(apperrors.ErrContextValue) + response.WriteJson(w, status, errorMessage, nil) + return + } + + monthVal := r.URL.Query().Get("month") + month, err := utils.ValidateMonthQueryParam(monthVal) + if err != nil { + slog.Error("error converting month value to integer", "error", err) + status, errorMessage := apperrors.MapError(apperrors.ErrContextValue) + response.WriteJson(w, status, errorMessage, nil) + return + } + + monthlyContributionSummary, err := h.contributionService.ListMonthlyContributionSummary(ctx, year, month, userId) + if err != nil { + slog.Error("error fetching contribution type summary for month", "error", err) + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) + return + } + + response.WriteJson(w, http.StatusOK, "contribution type overview for month fetched successfully", monthlyContributionSummary) +} diff --git a/backend/internal/app/contribution/service.go b/backend/internal/app/contribution/service.go new file mode 100644 index 00000000..610918ae --- /dev/null +++ b/backend/internal/app/contribution/service.go @@ -0,0 +1,307 @@ +package contribution + +import ( + "context" + "encoding/json" + "log/slog" + "net/http" + + "github.com/joshsoftware/code-curiosity-2025/internal/app/bigquery" + repoService "github.com/joshsoftware/code-curiosity-2025/internal/app/repository" + "github.com/joshsoftware/code-curiosity-2025/internal/app/transaction" + "github.com/joshsoftware/code-curiosity-2025/internal/app/user" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" + "github.com/joshsoftware/code-curiosity-2025/internal/repository" + "google.golang.org/api/iterator" +) + +// github event names +const ( + pullRequestEvent = "PullRequestEvent" + issuesEvent = "IssuesEvent" + pushEvent = "PushEvent" + issueCommentEvent = "IssueCommentEvent" +) + +// app contribution types +const ( + pullRequestMerged = "PullRequestMerged" + pullRequestOpened = "PullRequestOpened" + issueOpened = "IssueOpened" + issueClosed = "IssueClosed" + issueResolved = "IssueResolved" + pullRequestUpdated = "PullRequestUpdated" + issueComment = "IssueComment" + pullRequestComment = "PullRequestComment" +) + +// payload +const ( + payloadActionKey = "action" + payloadPullRequestKey = "pull_request" + PayloadMergedKey = "merged" + PayloadIssueKey = "issue" + PayloadStateReasonKey = "state_reason" + PayloadClosedKey = "closed" + PayloadOpenedKey = "opened" + PayloadNotPlannedKey = "not_planned" + PayloadCompletedKey = "completed" +) + +type service struct { + bigqueryService bigquery.Service + contributionRepository repository.ContributionRepository + repositoryService repoService.Service + userService user.Service + transactionService transaction.Service + httpClient *http.Client +} + +type Service interface { + ProcessFetchedContributions(ctx context.Context) error + ProcessEachContribution(ctx context.Context, contribution ContributionResponse) error + GetContributionType(ctx context.Context, contribution ContributionResponse) (string, error) + CreateContribution(ctx context.Context, contributionType string, contributionDetails ContributionResponse, repositoryId int, userId int) (Contribution, error) + HandleContributionCreation(ctx context.Context, repositoryID int, contribution ContributionResponse) (Contribution, error) + GetContributionScoreDetailsByContributionType(ctx context.Context, contributionType string) (ContributionScore, error) + FetchUserContributions(ctx context.Context) ([]Contribution, error) + GetContributionByGithubEventId(ctx context.Context, githubEventId string) (Contribution, error) + ListMonthlyContributionSummary(ctx context.Context, year int, monthParam int, userId int) ([]MonthlyContributionSummary, error) +} + +func NewService(bigqueryService bigquery.Service, contributionRepository repository.ContributionRepository, repositoryService repoService.Service, userService user.Service, transactionService transaction.Service, httpClient *http.Client) Service { + return &service{ + bigqueryService: bigqueryService, + contributionRepository: contributionRepository, + repositoryService: repositoryService, + userService: userService, + transactionService: transactionService, + httpClient: httpClient, + } +} + +func (s *service) ProcessFetchedContributions(ctx context.Context) error { + contributions, err := s.bigqueryService.FetchDailyContributions(ctx) + if err != nil { + slog.Error("error fetching daily contributions", "error", err) + return apperrors.ErrFetchingFromBigquery + } + + //using a local copy here to copy contribution so that I can implement retry mechanism in future + //thinking of batch processing to be implemented later on, to handle memory overflow + var fetchedContributions []ContributionResponse + + for { + var contribution ContributionResponse + err := contributions.Next(&contribution) + if err != nil { + if err == iterator.Done { + break + } + + slog.Error("error iterating contribution rows", "error", err) + return apperrors.ErrNextContribution + } + + fetchedContributions = append(fetchedContributions, contribution) + } + + for _, contribution := range fetchedContributions { + err := s.ProcessEachContribution(ctx, contribution) + if err != nil { + slog.Error("error processing contribution with github event id", "github event id", "error", contribution.ID, err) + return err + } + } + + return nil +} + +func (s *service) ProcessEachContribution(ctx context.Context, contribution ContributionResponse) error { + obtainedContribution, err := s.GetContributionByGithubEventId(ctx, contribution.ID) + if err != nil { + if err == apperrors.ErrContributionNotFound { + obtainedRepository, err := s.repositoryService.HandleRepositoryCreation(ctx, repoService.ContributionResponse(contribution)) + if err != nil { + slog.Error("error handling repository creation", "error", err) + return err + } + obtainedContribution, err = s.HandleContributionCreation(ctx, obtainedRepository.Id, contribution) + if err != nil { + slog.Error("error handling contribution creation", "error", err) + return err + } + } else { + slog.Error("error fetching contribution by github event id", "error", err) + return err + } + } + + _, err = s.transactionService.HandleTransactionCreation(ctx, transaction.Contribution(obtainedContribution)) + if err != nil { + slog.Error("error handling transaction creation", "error", err) + return err + } + + return nil +} + +func (s *service) GetContributionType(ctx context.Context, contribution ContributionResponse) (string, error) { + var contributionPayload map[string]interface{} + err := json.Unmarshal([]byte(contribution.Payload), &contributionPayload) + if err != nil { + slog.Warn("invalid payload", "error", err) + return "", err + } + + var action string + if actionVal, ok := contributionPayload[payloadActionKey]; ok { + action = actionVal.(string) + } + + var pullRequest map[string]interface{} + var isMerged bool + if pullRequestPayload, ok := contributionPayload[payloadPullRequestKey]; ok { + pullRequest = pullRequestPayload.(map[string]interface{}) + isMerged = pullRequest[PayloadMergedKey].(bool) + } + + var issue map[string]interface{} + var stateReason string + if issuePayload, ok := contributionPayload[PayloadIssueKey]; ok { + issue = issuePayload.(map[string]interface{}) + stateReason = issue[PayloadStateReasonKey].(string) + } + + var contributionType string + switch contribution.Type { + case pullRequestEvent: + if action == PayloadClosedKey && isMerged { + contributionType = pullRequestMerged + } else if action == PayloadOpenedKey { + contributionType = pullRequestOpened + } + + case issuesEvent: + if action == PayloadOpenedKey { + contributionType = issueOpened + } else if action == PayloadClosedKey && stateReason == PayloadNotPlannedKey { + contributionType = issueClosed + } else if action == PayloadClosedKey && stateReason == PayloadCompletedKey { + contributionType = issueResolved + } + + case pushEvent: + contributionType = pullRequestUpdated + + case issueCommentEvent: + contributionType = issueComment + + case pullRequestComment: + contributionType = pullRequestComment + } + + return contributionType, nil +} + +func (s *service) CreateContribution(ctx context.Context, contributionType string, contributionDetails ContributionResponse, repositoryId int, userId int) (Contribution, error) { + + contribution := Contribution{ + UserId: userId, + RepositoryId: repositoryId, + ContributionType: contributionType, + ContributedAt: contributionDetails.CreatedAt, + GithubEventId: contributionDetails.ID, + } + + contributionScoreDetails, err := s.GetContributionScoreDetailsByContributionType(ctx, contributionType) + if err != nil { + slog.Error("error occured while getting contribution score details", "error", err) + return Contribution{}, err + } + + contribution.ContributionScoreId = contributionScoreDetails.Id + contribution.BalanceChange = contributionScoreDetails.Score + + contributionResponse, err := s.contributionRepository.CreateContribution(ctx, nil, repository.Contribution(contribution)) + if err != nil { + slog.Error("error creating contribution", "error", err) + return Contribution{}, err + } + + return Contribution(contributionResponse), nil +} + +func (s *service) HandleContributionCreation(ctx context.Context, repositoryID int, contribution ContributionResponse) (Contribution, error) { + user, err := s.userService.GetUserByGithubId(ctx, contribution.ActorID) + if err != nil { + slog.Error("error getting user id", "error", err) + return Contribution{}, err + } + + contributionType, err := s.GetContributionType(ctx, contribution) + if err != nil { + slog.Error("error getting contribution type", "error", err) + return Contribution{}, err + } + + obtainedContribution, err := s.CreateContribution(ctx, contributionType, contribution, repositoryID, user.Id) + if err != nil { + slog.Error("error creating contribution", "error", err) + return Contribution{}, err + } + + return obtainedContribution, nil +} + +func (s *service) GetContributionScoreDetailsByContributionType(ctx context.Context, contributionType string) (ContributionScore, error) { + contributionScoreDetails, err := s.contributionRepository.GetContributionScoreDetailsByContributionType(ctx, nil, contributionType) + if err != nil { + slog.Error("error occured while getting contribution score details", "error", err) + return ContributionScore{}, err + } + + return ContributionScore(contributionScoreDetails), nil +} + +func (s *service) FetchUserContributions(ctx context.Context) ([]Contribution, error) { + userContributions, err := s.contributionRepository.FetchUserContributions(ctx, nil) + if err != nil { + slog.Error("error occured while fetching user contributions", "error", err) + return nil, err + } + + serviceContributions := make([]Contribution, len(userContributions)) + for i, c := range userContributions { + serviceContributions[i] = Contribution((c)) + } + + return serviceContributions, nil +} + +func (s *service) GetContributionByGithubEventId(ctx context.Context, githubEventId string) (Contribution, error) { + contribution, err := s.contributionRepository.GetContributionByGithubEventId(ctx, nil, githubEventId) + if err != nil { + slog.Error("error fetching contribution by github event id", "error", err) + return Contribution{}, err + } + + return Contribution(contribution), nil +} + +func (s *service) ListMonthlyContributionSummary(ctx context.Context, year int, month int, userId int) ([]MonthlyContributionSummary, error) { + + MonthlyContributionSummaries, err := s.contributionRepository.ListMonthlyContributionSummary(ctx, nil, year, month, userId) + if err != nil { + slog.Error("error fetching monthly contribution summary", "error", err) + return nil, err + } + + serviceMonthlyContributionSummaries := make([]MonthlyContributionSummary, len(MonthlyContributionSummaries)) + + for i, c := range MonthlyContributionSummaries { + serviceMonthlyContributionSummaries[i] = MonthlyContributionSummary(c) + } + + return serviceMonthlyContributionSummaries, nil +} diff --git a/backend/internal/app/cronJob/cleanupJob.go b/backend/internal/app/cronJob/cleanupJob.go new file mode 100644 index 00000000..d87b3c99 --- /dev/null +++ b/backend/internal/app/cronJob/cleanupJob.go @@ -0,0 +1,32 @@ +package cronJob + +import ( + "context" + + "github.com/joshsoftware/code-curiosity-2025/internal/app/user" +) + +type CleanupJob struct { + CronJob + userService user.Service +} + +func NewCleanupJob(userService user.Service) *CleanupJob { + return &CleanupJob{ + userService: userService, + CronJob: CronJob{Name: "User Cleanup Job Daily"}, + } +} + +func (c *CleanupJob) Schedule(s *CronSchedular) error { + _, err := s.cron.AddFunc("00 18 * * *", func() { c.Execute(context.Background(), c.run) }) + if err != nil { + return err + } + + return nil +} + +func (c *CleanupJob) run(ctx context.Context) { + c.userService.HardDeleteUsers(ctx) +} diff --git a/backend/internal/app/cronJob/cronjob.go b/backend/internal/app/cronJob/cronjob.go new file mode 100644 index 00000000..f8f3a219 --- /dev/null +++ b/backend/internal/app/cronJob/cronjob.go @@ -0,0 +1,24 @@ +package cronJob + +import ( + "context" + "log/slog" + "time" +) + +type Job interface { + Schedule(c *CronSchedular) error +} + +type CronJob struct { + Name string +} + +func (c *CronJob) Execute(ctx context.Context, fn func(context.Context)) { + slog.Info("cron job started at", "time ", time.Now()) + defer func() { + slog.Info("cron job completed") + }() + + fn(ctx) +} diff --git a/backend/internal/app/cronJob/dailyJob.go b/backend/internal/app/cronJob/dailyJob.go new file mode 100644 index 00000000..67f3e24c --- /dev/null +++ b/backend/internal/app/cronJob/dailyJob.go @@ -0,0 +1,32 @@ +package cronJob + +import ( + "context" + + "github.com/joshsoftware/code-curiosity-2025/internal/app/contribution" +) + +type DailyJob struct { + CronJob + contributionService contribution.Service +} + +func NewDailyJob(contributionService contribution.Service) *DailyJob { + return &DailyJob{ + contributionService: contributionService, + CronJob: CronJob{Name: "Fetch Contributions Daily"}, + } +} + +func (d *DailyJob) Schedule(s *CronSchedular) error { + _, err := s.cron.AddFunc("0 1 * * *", func() { d.Execute(context.Background(), d.run) }) + if err != nil { + return err + } + + return nil +} + +func (d *DailyJob) run(ctx context.Context) { + d.contributionService.ProcessFetchedContributions(ctx) +} diff --git a/backend/internal/app/cronJob/init.go b/backend/internal/app/cronJob/init.go new file mode 100644 index 00000000..77d9f78b --- /dev/null +++ b/backend/internal/app/cronJob/init.go @@ -0,0 +1,40 @@ +package cronJob + +import ( + "log/slog" + "time" + + "github.com/joshsoftware/code-curiosity-2025/internal/app/contribution" + "github.com/joshsoftware/code-curiosity-2025/internal/app/user" + "github.com/robfig/cron/v3" +) + +type CronSchedular struct { + cron *cron.Cron +} + +func NewCronSchedular() *CronSchedular { + location, err := time.LoadLocation("Asia/Kolkata") + if err != nil { + slog.Error("failed to load IST timezone", "error", err) + } + + return &CronSchedular{ + cron: cron.New(cron.WithLocation(location)), + } +} + +func (c *CronSchedular) InitCronJobs(contributionService contribution.Service, userService user.Service) { + jobs := []Job{ + NewDailyJob(contributionService), + NewCleanupJob(userService), + } + + for _, job := range jobs { + if err := job.Schedule(c); err != nil { + slog.Error("failed to execute cron job") + } + } + + c.cron.Start() +} diff --git a/backend/internal/app/dependencies.go b/backend/internal/app/dependencies.go new file mode 100644 index 00000000..bc4fa575 --- /dev/null +++ b/backend/internal/app/dependencies.go @@ -0,0 +1,71 @@ +package app + +import ( + "net/http" + + "github.com/jmoiron/sqlx" + "github.com/joshsoftware/code-curiosity-2025/internal/app/auth" + "github.com/joshsoftware/code-curiosity-2025/internal/app/badge" + "github.com/joshsoftware/code-curiosity-2025/internal/app/bigquery" + "github.com/joshsoftware/code-curiosity-2025/internal/app/contribution" + "github.com/joshsoftware/code-curiosity-2025/internal/app/github" + "github.com/joshsoftware/code-curiosity-2025/internal/app/goal" + repoService "github.com/joshsoftware/code-curiosity-2025/internal/app/repository" + "github.com/joshsoftware/code-curiosity-2025/internal/app/transaction" + "github.com/joshsoftware/code-curiosity-2025/internal/app/user" + "github.com/joshsoftware/code-curiosity-2025/internal/config" + + "github.com/joshsoftware/code-curiosity-2025/internal/repository" +) + +type Dependencies struct { + ContributionService contribution.Service + UserService user.Service + AuthHandler auth.Handler + UserHandler user.Handler + ContributionHandler contribution.Handler + RepositoryHandler repoService.Handler + GoalHandler goal.Handler + BadgeHandler badge.Handler + AppCfg config.AppConfig + Client config.Bigquery +} + +func InitDependencies(db *sqlx.DB, appCfg config.AppConfig, client config.Bigquery, httpClient *http.Client) Dependencies { + badgeRepository := repository.NewBadgeRepository(db) + goalRepository := repository.NewGoalRepository(db) + userRepository := repository.NewUserRepository(db) + contributionRepository := repository.NewContributionRepository(db) + repositoryRepository := repository.NewRepositoryRepository(db) + transactionRepository := repository.NewTransactionRepository(db) + + badgeService := badge.NewService(badgeRepository) + goalService := goal.NewService(goalRepository, contributionRepository, badgeService) + userService := user.NewService(userRepository, goalService) + authService := auth.NewService(userService, appCfg) + bigqueryService := bigquery.NewService(client, userRepository) + githubService := github.NewService(appCfg, httpClient) + repositoryService := repoService.NewService(repositoryRepository, githubService) + transactionService := transaction.NewService(transactionRepository, userService) + contributionService := contribution.NewService(bigqueryService, contributionRepository, repositoryService, userService, transactionService, httpClient) + + authHandler := auth.NewHandler(authService, appCfg) + userHandler := user.NewHandler(userService) + repositoryHandler := repoService.NewHandler(repositoryService, githubService) + contributionHandler := contribution.NewHandler(contributionService) + goalHandler := goal.NewHandler(goalService) + badgeHandler := badge.NewHandler(badgeService) + + return Dependencies{ + ContributionService: contributionService, + UserService: userService, + AuthHandler: authHandler, + UserHandler: userHandler, + RepositoryHandler: repositoryHandler, + ContributionHandler: contributionHandler, + GoalHandler: goalHandler, + BadgeHandler: badgeHandler, + AppCfg: appCfg, + Client: client, + } +} diff --git a/backend/internal/app/github/domain.go b/backend/internal/app/github/domain.go new file mode 100644 index 00000000..efdd4415 --- /dev/null +++ b/backend/internal/app/github/domain.go @@ -0,0 +1,30 @@ +package github + +import "time" + +const AuthorizationKey = "Authorization" + +type RepoOwner struct { + Login string `json:"login"` +} + +type FetchRepositoryDetailsResponse struct { + Id int `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + LanguagesURL string `json:"languages_url"` + UpdateDate time.Time `json:"updated_at"` + RepoOwnerName RepoOwner `json:"owner"` + ContributorsUrl string `json:"contributors_url"` + RepoUrl string `json:"html_url"` +} + +type RepoLanguages map[string]int + +type FetchRepoContributorsResponse struct { + Id int `json:"id"` + Name string `json:"login"` + AvatarUrl string `json:"avatar_url"` + GithubUrl string `json:"html_url"` + Contributions int `json:"contributions"` +} diff --git a/backend/internal/app/github/service.go b/backend/internal/app/github/service.go new file mode 100644 index 00000000..16637c03 --- /dev/null +++ b/backend/internal/app/github/service.go @@ -0,0 +1,93 @@ +package github + +import ( + "context" + "encoding/json" + "log/slog" + "net/http" + + "github.com/joshsoftware/code-curiosity-2025/internal/config" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/utils" +) + +type service struct { + appCfg config.AppConfig + httpClient *http.Client +} + +type Service interface { + configureGithubApiHeaders() map[string]string + FetchRepositoryDetails(ctx context.Context, getUserRepoDetailsUrl string) (FetchRepositoryDetailsResponse, error) + FetchRepositoryLanguages(ctx context.Context, getRepoLanguagesURL string) (RepoLanguages, error) + FetchRepositoryContributors(ctx context.Context, getRepoContributorsURl string) ([]FetchRepoContributorsResponse, error) +} + +func NewService(appCfg config.AppConfig, httpClient *http.Client) Service { + return &service{ + appCfg: appCfg, + httpClient: httpClient, + } +} + +func (s *service) configureGithubApiHeaders() map[string]string { + return map[string]string{ + AuthorizationKey: s.appCfg.GithubPersonalAccessToken, + } +} + +func (s *service) FetchRepositoryDetails(ctx context.Context, getUserRepoDetailsUrl string) (FetchRepositoryDetailsResponse, error) { + headers := s.configureGithubApiHeaders() + + body, err := utils.DoGet(s.httpClient, getUserRepoDetailsUrl, headers) + if err != nil { + slog.Error("error making a GET request", "error", err) + return FetchRepositoryDetailsResponse{}, err + } + + var repoDetails FetchRepositoryDetailsResponse + err = json.Unmarshal(body, &repoDetails) + if err != nil { + slog.Error("error unmarshalling fetch repository details body", "error", err) + return FetchRepositoryDetailsResponse{}, err + } + + return repoDetails, nil +} + +func (s *service) FetchRepositoryLanguages(ctx context.Context, getRepoLanguagesURL string) (RepoLanguages, error) { + headers := s.configureGithubApiHeaders() + + body, err := utils.DoGet(s.httpClient, getRepoLanguagesURL, headers) + if err != nil { + slog.Error("error making a GET request", "error", err) + return RepoLanguages{}, err + } + + var repoLanguages RepoLanguages + err = json.Unmarshal(body, &repoLanguages) + if err != nil { + slog.Error("error unmarshalling fetch repository languages body", "error", err) + return RepoLanguages{}, err + } + + return repoLanguages, nil +} + +func (s *service) FetchRepositoryContributors(ctx context.Context, getRepoContributorsURl string) ([]FetchRepoContributorsResponse, error) { + headers := s.configureGithubApiHeaders() + + body, err := utils.DoGet(s.httpClient, getRepoContributorsURl, headers) + if err != nil { + slog.Error("error making a GET request", "error", err) + return []FetchRepoContributorsResponse{}, err + } + + var repoContributors []FetchRepoContributorsResponse + err = json.Unmarshal(body, &repoContributors) + if err != nil { + slog.Error("error unmarshalling fetch contributors body", "error", err) + return nil, err + } + + return repoContributors, nil +} diff --git a/backend/internal/app/goal/domain.go b/backend/internal/app/goal/domain.go new file mode 100644 index 00000000..163d6157 --- /dev/null +++ b/backend/internal/app/goal/domain.go @@ -0,0 +1,26 @@ +package goal + +import "time" + +type Goal struct { + Id int + Level string + CreatedAt time.Time + UpdatedAt time.Time +} + +type GoalContribution struct { + Id int + GoalId int + ContributionScoreId int + TargetCount int + IsCustom bool + SetByUserId int + CreatedAt time.Time + UpdatedAt time.Time +} + +type CustomGoalLevelTarget struct { + ContributionType string `json:"contribution_type"` + Target int `json:"target"` +} diff --git a/backend/internal/app/goal/handler.go b/backend/internal/app/goal/handler.go new file mode 100644 index 00000000..4fb12277 --- /dev/null +++ b/backend/internal/app/goal/handler.go @@ -0,0 +1,117 @@ +package goal + +import ( + "encoding/json" + "log/slog" + "net/http" + + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/middleware" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/response" +) + +type handler struct { + goalService Service +} + +type Handler interface { + ListGoalLevels(w http.ResponseWriter, r *http.Request) + ListGoalLevelTargets(w http.ResponseWriter, r *http.Request) + CreateCustomGoalLevelTarget(w http.ResponseWriter, r *http.Request) + ListGoalLevelAchievedTarget(w http.ResponseWriter, r *http.Request) +} + +func NewHandler(goalService Service) Handler { + return &handler{ + goalService: goalService, + } +} + +func (h *handler) ListGoalLevels(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + gaols, err := h.goalService.ListGoalLevels(ctx) + if err != nil { + slog.Error("error fetching users conributed repos", "error", err) + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) + return + } + + response.WriteJson(w, http.StatusOK, "goal levels fetched successfully", gaols) +} + +func (h *handler) ListGoalLevelTargets(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + userIdCtxVal := ctx.Value(middleware.UserIdKey) + userId, ok := userIdCtxVal.(int) + if !ok { + slog.Error("error obtaining user id from context") + status, errorMessage := apperrors.MapError(apperrors.ErrContextValue) + response.WriteJson(w, status, errorMessage, nil) + return + } + + goalLevelTargets, err := h.goalService.ListGoalLevelTargetDetail(ctx, userId) + if err != nil { + slog.Error("error fetching goal level targets", "error", err) + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) + return + } + + response.WriteJson(w, http.StatusOK, "goal level targets fetched successfully", goalLevelTargets) +} + +func (h *handler) CreateCustomGoalLevelTarget(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + userIdCtxVal := ctx.Value(middleware.UserIdKey) + userId, ok := userIdCtxVal.(int) + if !ok { + slog.Error("error obtaining user id from context") + status, errorMessage := apperrors.MapError(apperrors.ErrContextValue) + response.WriteJson(w, status, errorMessage, nil) + return + } + + var customGoalLevelTarget []CustomGoalLevelTarget + err := json.NewDecoder(r.Body).Decode(&customGoalLevelTarget) + if err != nil { + slog.Error(apperrors.ErrFailedMarshal.Error(), "error", err) + response.WriteJson(w, http.StatusBadRequest, apperrors.ErrInvalidRequestBody.Error(), nil) + return + } + + createdCustomGoalLevelTargets, err := h.goalService.CreateCustomGoalLevelTarget(ctx, userId, customGoalLevelTarget) + if err != nil { + slog.Error(apperrors.ErrFailedMarshal.Error(), "error", err) + response.WriteJson(w, http.StatusBadRequest, err.Error(), nil) + return + } + + response.WriteJson(w, http.StatusOK, "custom goal level targets created successfully", createdCustomGoalLevelTargets) +} + +func (h *handler) ListGoalLevelAchievedTarget(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + userIdCtxVal := ctx.Value(middleware.UserIdKey) + userId, ok := userIdCtxVal.(int) + if !ok { + slog.Error("error obtaining user id from context") + status, errorMessage := apperrors.MapError(apperrors.ErrContextValue) + response.WriteJson(w, status, errorMessage, nil) + return + } + + goalLevelAchievedTarget, err := h.goalService.ListGoalLevelAchievedTarget(ctx, userId) + if err != nil { + slog.Error("error failed to list goal level achieved targets", "error", err) + response.WriteJson(w, http.StatusBadRequest, err.Error(), nil) + return + } + + response.WriteJson(w, http.StatusOK, "goal level achieved targets fetched successfully", goalLevelAchievedTarget) +} diff --git a/backend/internal/app/goal/service.go b/backend/internal/app/goal/service.go new file mode 100644 index 00000000..34b38d1e --- /dev/null +++ b/backend/internal/app/goal/service.go @@ -0,0 +1,164 @@ +package goal + +import ( + "context" + "log/slog" + "time" + + "github.com/joshsoftware/code-curiosity-2025/internal/app/badge" + "github.com/joshsoftware/code-curiosity-2025/internal/repository" +) + +type service struct { + goalRepository repository.GoalRepository + contributionRepository repository.ContributionRepository + badgeService badge.Service +} + +type Service interface { + ListGoalLevels(ctx context.Context) ([]Goal, error) + GetGoalIdByGoalLevel(ctx context.Context, level string) (int, error) + ListGoalLevelTargetDetail(ctx context.Context, userId int) ([]GoalContribution, error) + CreateCustomGoalLevelTarget(ctx context.Context, userId int, customGoalLevelTarget []CustomGoalLevelTarget) ([]GoalContribution, error) + ListGoalLevelAchievedTarget(ctx context.Context, userId int) (map[string]int, error) +} + +func NewService(goalRepository repository.GoalRepository, contributionRepository repository.ContributionRepository, badgeService badge.Service) Service { + return &service{ + goalRepository: goalRepository, + contributionRepository: contributionRepository, + badgeService: badgeService, + } +} + +func (s *service) ListGoalLevels(ctx context.Context) ([]Goal, error) { + goals, err := s.goalRepository.ListGoalLevels(ctx, nil) + if err != nil { + slog.Error("error fetching goal levels", "error", err) + return nil, err + } + + serviceGoals := make([]Goal, len(goals)) + + for i, g := range goals { + serviceGoals[i] = Goal(g) + } + + return serviceGoals, nil +} + +func (s *service) GetGoalIdByGoalLevel(ctx context.Context, level string) (int, error) { + goalId, err := s.goalRepository.GetGoalIdByGoalLevel(ctx, nil, level) + + if err != nil { + slog.Error("failed to get goal id by goal level", "error", err) + return 0, err + } + + return goalId, err +} + +func (s *service) ListGoalLevelTargetDetail(ctx context.Context, userId int) ([]GoalContribution, error) { + goalLevelTargets, err := s.goalRepository.ListUserGoalLevelTargets(ctx, nil, userId) + if err != nil { + slog.Error("error fetching goal level targets", "error", err) + return nil, err + } + + serviceGoalLevelTargets := make([]GoalContribution, len(goalLevelTargets)) + for i, g := range goalLevelTargets { + serviceGoalLevelTargets[i] = GoalContribution(g) + } + + return serviceGoalLevelTargets, nil +} + +func (s *service) CreateCustomGoalLevelTarget(ctx context.Context, userID int, customGoalLevelTarget []CustomGoalLevelTarget) ([]GoalContribution, error) { + customGoalLevelId, err := s.GetGoalIdByGoalLevel(ctx, "Custom") + if err != nil { + slog.Error("error fetching custom goal level id", "error", err) + return nil, err + } + var goalContributions []GoalContribution + + goalContributionInfo := make([]GoalContribution, len(customGoalLevelTarget)) + for i, c := range customGoalLevelTarget { + goalContributionInfo[i].GoalId = customGoalLevelId + + contributionScoreDetails, err := s.contributionRepository.GetContributionScoreDetailsByContributionType(ctx, nil, c.ContributionType) + if err != nil { + slog.Error("error fetching contribution score details by type", "error", err) + return nil, err + } + + goalContributionInfo[i].ContributionScoreId = contributionScoreDetails.Id + goalContributionInfo[i].TargetCount = c.Target + goalContributionInfo[i].SetByUserId = userID + + goalContribution, err := s.goalRepository.CreateCustomGoalLevelTarget(ctx, nil, repository.GoalContribution(goalContributionInfo[i])) + if err != nil { + slog.Error("error creating custom goal level target", "error", err) + return nil, err + } + + goalContributions = append(goalContributions, GoalContribution(goalContribution)) + } + + return goalContributions, nil +} + +func (s *service) ListGoalLevelAchievedTarget(ctx context.Context, userId int) (map[string]int, error) { + goalLevelSetTargets, err := s.goalRepository.ListUserGoalLevelTargets(ctx, nil, userId) + if err != nil { + slog.Error("error fetching goal level targets", "error", err) + return nil, err + } + + contributionTypes := make([]CustomGoalLevelTarget, len(goalLevelSetTargets)) + for i, g := range goalLevelSetTargets { + contributionTypes[i].ContributionType, err = s.contributionRepository.GetContributionTypeByContributionScoreId(ctx, nil, g.ContributionScoreId) + if err != nil { + slog.Error("error fetching contribution type by contribution score id", "error", err) + return nil, err + } + + contributionTypes[i].Target = g.TargetCount + } + + year := int(time.Now().Year()) + month := int(time.Now().Month()) + monthlyContributionCount, err := s.contributionRepository.ListMonthlyContributionSummary(ctx, nil, year, month, userId) + if err != nil { + slog.Error("error fetching monthly contribution count", "error", err) + return nil, err + } + + contributionsAchievedTarget := make(map[string]int, len(monthlyContributionCount)) + + for _, m := range monthlyContributionCount { + contributionsAchievedTarget[m.Type] = m.Count + } + + var completedTarget int + for _, c := range contributionTypes { + if c.Target == contributionsAchievedTarget[c.ContributionType] { + completedTarget += 1 + } + } + + if completedTarget == len(goalLevelSetTargets) { + userGoalLevel, err := s.goalRepository.GetUserActiveGoalLevel(ctx, nil, userId) + if err != nil { + slog.Error("error fetching user active gaol level", "error", err) + return nil, err + } + + _, err = s.badgeService.HandleBadgeCreation(ctx, userId, userGoalLevel) + if err != nil { + slog.Error("error handling user badge creation", "error", err) + return nil, err + } + } + + return contributionsAchievedTarget, nil +} diff --git a/backend/internal/app/repository/domain.go b/backend/internal/app/repository/domain.go new file mode 100644 index 00000000..208f2976 --- /dev/null +++ b/backend/internal/app/repository/domain.go @@ -0,0 +1,56 @@ +package repository + +import "time" + +type Repository struct { + Id int + GithubRepoId int + RepoName string + Description string + LanguagesUrl string + RepoUrl string + OwnerName string + UpdateDate time.Time + ContributorsUrl string + CreatedAt time.Time + UpdatedAt time.Time +} + +type RepoLanguages map[string]int + +type FetchUsersContributedReposResponse struct { + Repository + Languages []string + TotalCoinsEarned int +} + +type ContributionResponse struct { + ID string `bigquery:"id"` + Type string `bigquery:"type"` + ActorID int `bigquery:"actor_id"` + ActorLogin string `bigquery:"actor_login"` + RepoID int `bigquery:"repo_id"` + RepoName string `bigquery:"repo_name"` + RepoUrl string `bigquery:"repo_url"` + Payload string `bigquery:"payload"` + CreatedAt time.Time `bigquery:"created_at"` +} + +type Contribution struct { + Id int + UserId int + RepositoryId int + ContributionScoreId int + ContributionType string + BalanceChange int + ContributedAt time.Time + GithubEventId string + CreatedAt time.Time + UpdatedAt time.Time +} + +type LanguagePercent struct { + Name string + Bytes int + Percentage float64 +} diff --git a/backend/internal/app/repository/handler.go b/backend/internal/app/repository/handler.go new file mode 100644 index 00000000..f040a31c --- /dev/null +++ b/backend/internal/app/repository/handler.go @@ -0,0 +1,161 @@ +package repository + +import ( + "log/slog" + "net/http" + "strconv" + + "github.com/joshsoftware/code-curiosity-2025/internal/app/github" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/response" +) + +type handler struct { + repositoryService Service + githubService github.Service +} + +type Handler interface { + FetchUsersContributedRepos(w http.ResponseWriter, r *http.Request) + FetchParticularRepoDetails(w http.ResponseWriter, r *http.Request) + FetchUserContributionsInRepo(w http.ResponseWriter, r *http.Request) + FetchLanguagePercentInRepo(w http.ResponseWriter, r *http.Request) +} + +func NewHandler(repositoryService Service, githubService github.Service) Handler { + return &handler{ + repositoryService: repositoryService, + githubService: githubService, + } +} + +func (h *handler) FetchUsersContributedRepos(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + client := &http.Client{} + + usersContributedRepos, err := h.repositoryService.FetchUsersContributedRepos(ctx, client) + if err != nil { + slog.Error("error fetching users conributed repos", "error", err) + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) + return + } + + response.WriteJson(w, http.StatusOK, "users contributed repositories fetched successfully", usersContributedRepos) +} + +func (h *handler) FetchParticularRepoDetails(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + repoIdPath := r.PathValue("repo_id") + repoId, err := strconv.Atoi(repoIdPath) + if err != nil { + slog.Error("error getting repo id from request url", "error", err) + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) + return + } + + repoDetails, err := h.repositoryService.GetRepoByRepoId(ctx, repoId) + if err != nil { + slog.Error("error fetching particular repo details", "error", err) + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) + return + } + + response.WriteJson(w, http.StatusOK, "repository details fetched successfully", repoDetails) +} + +func (h *handler) FetchParticularRepoContributors(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + repoIdPath := r.PathValue("repo_id") + repoId, err := strconv.Atoi(repoIdPath) + if err != nil { + slog.Error("error getting repo id from request url", "error", err) + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) + return + } + + repoDetails, err := h.repositoryService.GetRepoByRepoId(ctx, repoId) + if err != nil { + slog.Error("error fetching particular repo details", "error", err) + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) + return + } + + repoContributors, err := h.githubService.FetchRepositoryContributors(ctx, repoDetails.ContributorsUrl) + if err != nil { + slog.Error("error fetching repo contributors", "error", err) + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) + return + } + + response.WriteJson(w, http.StatusOK, "contributors for repo fetched successfully", repoContributors) +} + +func (h *handler) FetchUserContributionsInRepo(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + repoIdPath := r.PathValue("repo_id") + repoId, err := strconv.Atoi(repoIdPath) + if err != nil { + slog.Error("error getting repo id from request url", "error", err) + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) + return + } + + usersContributionsInRepo, err := h.repositoryService.FetchUserContributionsInRepo(ctx, repoId) + if err != nil { + slog.Error("error fetching users contribution in repository", "error", err) + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) + return + } + + response.WriteJson(w, http.StatusOK, "users contribution for repository fetched successfully", usersContributionsInRepo) +} + +func (h *handler) FetchLanguagePercentInRepo(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + repoIdPath := r.PathValue("repo_id") + repoId, err := strconv.Atoi(repoIdPath) + if err != nil { + slog.Error("error getting repo id from request url", "error", err) + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) + return + } + + repoDetails, err := h.repositoryService.GetRepoByRepoId(ctx, repoId) + if err != nil { + slog.Error("error fetching particular repo details", "error", err) + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) + return + } + + repoLanguages, err := h.githubService.FetchRepositoryLanguages(ctx, repoDetails.LanguagesUrl) + if err != nil { + slog.Error("error fetching particular repo languages", "error", err) + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) + return + } + + langPercent, err := h.repositoryService.CalculateLanguagePercentInRepo(ctx, RepoLanguages(repoLanguages)) + if err != nil { + slog.Error("error fetching particular repo languages", "error", err) + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) + return + } + + response.WriteJson(w, http.StatusOK, "language percentages for repo fetched successfully", langPercent) +} diff --git a/backend/internal/app/repository/service.go b/backend/internal/app/repository/service.go new file mode 100644 index 00000000..e73b75f3 --- /dev/null +++ b/backend/internal/app/repository/service.go @@ -0,0 +1,167 @@ +package repository + +import ( + "context" + "log/slog" + "math" + "net/http" + + "github.com/joshsoftware/code-curiosity-2025/internal/app/github" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" + "github.com/joshsoftware/code-curiosity-2025/internal/repository" +) + +type service struct { + repositoryRepository repository.RepositoryRepository + githubService github.Service +} + +type Service interface { + GetRepoByGithubId(ctx context.Context, githubRepoId int) (Repository, error) + GetRepoByRepoId(ctx context.Context, repoId int) (Repository, error) + CreateRepository(ctx context.Context, repoGithubId int, ContributionRepoDetailsUrl string) (Repository, error) + HandleRepositoryCreation(ctx context.Context, contribution ContributionResponse) (Repository, error) + FetchUsersContributedRepos(ctx context.Context, client *http.Client) ([]FetchUsersContributedReposResponse, error) + FetchUserContributionsInRepo(ctx context.Context, githubRepoId int) ([]Contribution, error) + CalculateLanguagePercentInRepo(ctx context.Context, repoLanguages RepoLanguages) ([]LanguagePercent, error) +} + +func NewService(repositoryRepository repository.RepositoryRepository, githubService github.Service) Service { + return &service{ + repositoryRepository: repositoryRepository, + githubService: githubService, + } +} + +func (s *service) GetRepoByGithubId(ctx context.Context, repoGithubId int) (Repository, error) { + repoDetails, err := s.repositoryRepository.GetRepoByGithubId(ctx, nil, repoGithubId) + if err != nil { + slog.Error("failed to get repository by repo github id", "error", err) + return Repository{}, err + } + + return Repository(repoDetails), nil +} + +func (s *service) GetRepoByRepoId(ctx context.Context, repobId int) (Repository, error) { + repoDetails, err := s.repositoryRepository.GetRepoByRepoId(ctx, nil, repobId) + if err != nil { + slog.Error("failed to get repository by repo id", "error", err) + return Repository{}, err + } + + return Repository(repoDetails), nil +} + +func (s *service) CreateRepository(ctx context.Context, repoGithubId int, ContributionRepoDetailsUrl string) (Repository, error) { + repo, err := s.githubService.FetchRepositoryDetails(ctx, ContributionRepoDetailsUrl) + if err != nil { + slog.Error("error fetching user repositories details", "error", err) + return Repository{}, err + } + + createRepo := Repository{ + GithubRepoId: repoGithubId, + RepoName: repo.Name, + RepoUrl: repo.RepoUrl, + Description: repo.Description, + LanguagesUrl: repo.LanguagesURL, + OwnerName: repo.RepoOwnerName.Login, + UpdateDate: repo.UpdateDate, + ContributorsUrl: repo.ContributorsUrl, + } + repositoryCreated, err := s.repositoryRepository.CreateRepository(ctx, nil, repository.Repository(createRepo)) + if err != nil { + slog.Error("failed to create repository", "error", err) + return Repository{}, err + } + + return Repository(repositoryCreated), nil +} + +func (s *service) HandleRepositoryCreation(ctx context.Context, contribution ContributionResponse) (Repository, error) { + obtainedRepository, err := s.GetRepoByGithubId(ctx, contribution.RepoID) + if err != nil { + if err == apperrors.ErrRepoNotFound { + obtainedRepository, err = s.CreateRepository(ctx, contribution.RepoID, contribution.RepoUrl) + if err != nil { + slog.Error("error creating repository", "error", err) + return Repository{}, err + } + } else { + slog.Error("error fetching repo by repo id", "error", err) + return Repository{}, err + } + } + + return obtainedRepository, nil +} + +func (s *service) FetchUsersContributedRepos(ctx context.Context, client *http.Client) ([]FetchUsersContributedReposResponse, error) { + usersContributedRepos, err := s.repositoryRepository.FetchUsersContributedRepos(ctx, nil) + if err != nil { + slog.Error("error fetching users conributed repos", "error", err) + return nil, err + } + + fetchUsersContributedReposResponse := make([]FetchUsersContributedReposResponse, len(usersContributedRepos)) + + for i, usersContributedRepo := range usersContributedRepos { + fetchUsersContributedReposResponse[i].Repository = Repository(usersContributedRepo) + + contributedRepoLanguages, err := s.githubService.FetchRepositoryLanguages(ctx, usersContributedRepo.LanguagesUrl) + if err != nil { + slog.Error("error fetching languages for repository", "error", err) + return nil, err + } + + for language := range contributedRepoLanguages { + fetchUsersContributedReposResponse[i].Languages = append(fetchUsersContributedReposResponse[i].Languages, language) + } + + userRepoTotalCoins, err := s.repositoryRepository.GetUserRepoTotalCoins(ctx, nil, usersContributedRepo.Id) + if err != nil { + slog.Error("error calculating total coins earned by user for the repository", "error", err) + return nil, err + } + + fetchUsersContributedReposResponse[i].TotalCoinsEarned = userRepoTotalCoins + } + + return fetchUsersContributedReposResponse, nil +} + +func (s *service) FetchUserContributionsInRepo(ctx context.Context, githubRepoId int) ([]Contribution, error) { + userContributionsInRepo, err := s.repositoryRepository.FetchUserContributionsInRepo(ctx, nil, githubRepoId) + if err != nil { + slog.Error("error fetching users contribution in repository", "error", err) + return nil, err + } + + serviceUserContributionsInRepo := make([]Contribution, len(userContributionsInRepo)) + for i, c := range userContributionsInRepo { + serviceUserContributionsInRepo[i] = Contribution(c) + } + + return serviceUserContributionsInRepo, nil +} + +func (s *service) CalculateLanguagePercentInRepo(ctx context.Context, repoLanguages RepoLanguages) ([]LanguagePercent, error) { + var total int + for _, bytes := range repoLanguages { + total += bytes + } + + var langPercent []LanguagePercent + + for lang, bytes := range repoLanguages { + percentage := (float64(bytes) / float64(total)) * 100 + langPercent = append(langPercent, LanguagePercent{ + Name: lang, + Bytes: bytes, + Percentage: math.Round(percentage*10) / 10, + }) + } + + return langPercent, nil +} diff --git a/backend/internal/app/router.go b/backend/internal/app/router.go new file mode 100644 index 00000000..c1bf5642 --- /dev/null +++ b/backend/internal/app/router.go @@ -0,0 +1,44 @@ +package app + +import ( + "net/http" + + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/middleware" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/response" +) + +func NewRouter(deps Dependencies) http.Handler { + router := http.NewServeMux() + + router.HandleFunc("GET /api/v1/health", func(w http.ResponseWriter, r *http.Request) { + response.WriteJson(w, http.StatusOK, "Server is up and running..", nil) + }) + + router.HandleFunc("GET /api/v1/auth/github", deps.AuthHandler.GithubOAuthLoginUrl) + router.HandleFunc("GET /api/v1/auth/github/callback", deps.AuthHandler.GithubOAuthLoginCallback) + router.HandleFunc("GET /api/v1/auth/user", middleware.Authentication(deps.AuthHandler.GetLoggedInUser, deps.AppCfg)) + + router.HandleFunc("PATCH /api/v1/user/email", middleware.Authentication(deps.UserHandler.UpdateUserEmail, deps.AppCfg)) + router.HandleFunc("DELETE /api/v1/user/delete/{user_id}", middleware.Authentication(deps.UserHandler.SoftDeleteUser, deps.AppCfg)) + router.HandleFunc("PATCH /api/v1/user/goal/level", middleware.Authentication(deps.UserHandler.UpdateCurrentActiveGoalId, deps.AppCfg)) + + router.HandleFunc("GET /api/v1/user/contributions/all", middleware.Authentication(deps.ContributionHandler.FetchUserContributions, deps.AppCfg)) + router.HandleFunc("GET /api/v1/user/overview", middleware.Authentication(deps.ContributionHandler.ListMonthlyContributionSummary, deps.AppCfg)) + + router.HandleFunc("GET /api/v1/user/repositories", middleware.Authentication(deps.RepositoryHandler.FetchUsersContributedRepos, deps.AppCfg)) + router.HandleFunc("GET /api/v1/user/repositories/{repo_id}", middleware.Authentication(deps.RepositoryHandler.FetchParticularRepoDetails, deps.AppCfg)) + router.HandleFunc("GET /api/v1/user/repositories/contributions/recent/{repo_id}", middleware.Authentication(deps.RepositoryHandler.FetchUserContributionsInRepo, deps.AppCfg)) + router.HandleFunc("GET /api/v1/user/repositories/languages/{repo_id}", middleware.Authentication(deps.RepositoryHandler.FetchLanguagePercentInRepo, deps.AppCfg)) + + router.HandleFunc("GET /api/v1/leaderboard", middleware.Authentication(deps.UserHandler.ListUserRanks, deps.AppCfg)) + router.HandleFunc("GET /api/v1/user/leaderboard", middleware.Authentication(deps.UserHandler.GetCurrentUserRank, deps.AppCfg)) + + router.HandleFunc("GET /api/v1/user/goal/level", middleware.Authentication(deps.GoalHandler.ListGoalLevels, deps.AppCfg)) + router.HandleFunc("GET /api/v1/user/goal/level/targets", middleware.Authentication(deps.GoalHandler.ListGoalLevelTargets, deps.AppCfg)) + router.HandleFunc("POST /api/v1/user/goal/level/custom/targets", middleware.Authentication(deps.GoalHandler.CreateCustomGoalLevelTarget, deps.AppCfg)) + router.HandleFunc("GET /api/v1/user/goal/level/targets/achieved", middleware.Authentication(deps.GoalHandler.ListGoalLevelAchievedTarget, deps.AppCfg)) + + router.HandleFunc("GET /api/v1/user/badges", middleware.Authentication(deps.BadgeHandler.GetBadgeDetailsOfUser, deps.AppCfg)) + + return middleware.CorsMiddleware(router, deps.AppCfg) +} diff --git a/backend/internal/app/transaction/domain.go b/backend/internal/app/transaction/domain.go new file mode 100644 index 00000000..6a02f131 --- /dev/null +++ b/backend/internal/app/transaction/domain.go @@ -0,0 +1,28 @@ +package transaction + +import "time" + +type Transaction struct { + Id int + UserId int + ContributionId int + IsRedeemed bool + IsGained bool + TransactedBalance int + TransactedAt time.Time + CreatedAt time.Time + UpdatedAt time.Time +} + +type Contribution struct { + Id int + UserId int + RepositoryId int + ContributionScoreId int + ContributionType string + BalanceChange int + ContributedAt time.Time + GithubEventId string + CreatedAt time.Time + UpdatedAt time.Time +} diff --git a/backend/internal/app/transaction/service.go b/backend/internal/app/transaction/service.go new file mode 100644 index 00000000..59b4f177 --- /dev/null +++ b/backend/internal/app/transaction/service.go @@ -0,0 +1,109 @@ +package transaction + +import ( + "context" + "log/slog" + + "github.com/joshsoftware/code-curiosity-2025/internal/app/user" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/middleware" + "github.com/joshsoftware/code-curiosity-2025/internal/repository" +) + +type service struct { + transactionRepository repository.TransactionRepository + userService user.Service +} + +type Service interface { + CreateTransaction(ctx context.Context, transactionInfo Transaction) (Transaction, error) + GetTransactionByContributionId(ctx context.Context, contributionId int) (Transaction, error) + CreateTransactionForContribution(ctx context.Context, contribution Contribution) (Transaction, error) + HandleTransactionCreation(ctx context.Context, contribution Contribution) (Transaction, error) +} + +func NewService(transactionRepository repository.TransactionRepository, userService user.Service) Service { + return &service{ + transactionRepository: transactionRepository, + userService: userService, + } +} + +func (s *service) CreateTransaction(ctx context.Context, transactionInfo Transaction) (Transaction, error) { + tx, err := s.transactionRepository.BeginTx(ctx) + if err != nil { + slog.Error("failed to start transaction creation") + return Transaction{}, err + } + + ctx = middleware.EmbedTxInContext(ctx, tx) + + defer func() { + if txErr := s.transactionRepository.HandleTransaction(ctx, tx, err); txErr != nil { + slog.Error("failed to handle transaction", "error", txErr) + err = txErr + } + }() + + transaction, err := s.transactionRepository.CreateTransaction(ctx, tx, repository.Transaction(transactionInfo)) + if err != nil { + slog.Error("error occured while creating transaction", "error", err) + return Transaction{}, err + } + + err = s.userService.UpdateUserCurrentBalance(ctx, user.Transaction(transaction)) + if err != nil { + slog.Error("error occured while updating user current balance", "error", err) + return Transaction{}, err + } + + return Transaction(transaction), nil +} + +func (s *service) GetTransactionByContributionId(ctx context.Context, contributionId int) (Transaction, error) { + transaction, err := s.transactionRepository.GetTransactionByContributionId(ctx, nil, contributionId) + if err != nil { + slog.Error("error fetching transaction using contribution id", "error", err) + return Transaction{}, err + } + + return Transaction(transaction), nil +} + +func (s *service) CreateTransactionForContribution(ctx context.Context, contribution Contribution) (Transaction, error) { + transactionInfo := Transaction{ + UserId: contribution.UserId, + ContributionId: contribution.Id, + IsRedeemed: false, + IsGained: true, + TransactedBalance: contribution.BalanceChange, + TransactedAt: contribution.ContributedAt, + } + transaction, err := s.CreateTransaction(ctx, transactionInfo) + if err != nil { + slog.Error("error creating transaction for current contribution", "error", err) + return Transaction{}, err + } + + return transaction, nil +} + +func (s *service) HandleTransactionCreation(ctx context.Context, contribution Contribution) (Transaction, error) { + var transaction Transaction + + transaction, err := s.GetTransactionByContributionId(ctx, contribution.Id) + if err != nil { + if err == apperrors.ErrTransactionNotFound { + transaction, err = s.CreateTransactionForContribution(ctx, contribution) + if err != nil { + slog.Error("error creating transaction for exisiting contribution", "error", err) + return Transaction{}, err + } + } else { + slog.Error("error fetching transaction", "error", err) + return Transaction{}, err + } + } + + return transaction, nil +} diff --git a/internal/app/user/domain.go b/backend/internal/app/user/domain.go similarity index 64% rename from internal/app/user/domain.go rename to backend/internal/app/user/domain.go index e2d9e6c7..087f25f4 100644 --- a/internal/app/user/domain.go +++ b/backend/internal/app/user/domain.go @@ -18,8 +18,8 @@ type User struct { Password string `json:"password"` IsDeleted bool `json:"is_deleted"` DeletedAt sql.NullTime `json:"deleted_at"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } type CreateUserRequestBody struct { @@ -33,3 +33,27 @@ type CreateUserRequestBody struct { type Email struct { Email string `json:"email"` } + +type Transaction struct { + Id int + UserId int + ContributionId int + IsRedeemed bool + IsGained bool + TransactedBalance int + TransactedAt time.Time + CreatedAt time.Time + UpdatedAt time.Time +} + +type LeaderboardUser struct { + Id int + GithubUsername string + AvatarUrl string + CurrentBalance int + Rank int +} + +type GoalLevel struct { + Level string `json:"level"` +} diff --git a/backend/internal/app/user/handler.go b/backend/internal/app/user/handler.go new file mode 100644 index 00000000..2d1bfcdc --- /dev/null +++ b/backend/internal/app/user/handler.go @@ -0,0 +1,144 @@ +package user + +import ( + "encoding/json" + "log/slog" + "net/http" + + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/middleware" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/response" +) + +type handler struct { + userService Service +} + +type Handler interface { + UpdateUserEmail(w http.ResponseWriter, r *http.Request) + SoftDeleteUser(w http.ResponseWriter, r *http.Request) + ListUserRanks(w http.ResponseWriter, r *http.Request) + GetCurrentUserRank(w http.ResponseWriter, r *http.Request) + UpdateCurrentActiveGoalId(w http.ResponseWriter, r *http.Request) +} + +func NewHandler(userService Service) Handler { + return &handler{ + userService: userService, + } +} + +func (h *handler) UpdateUserEmail(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + var requestBody Email + err := json.NewDecoder(r.Body).Decode(&requestBody) + if err != nil { + slog.Error(apperrors.ErrFailedMarshal.Error(), "error", err) + response.WriteJson(w, http.StatusBadRequest, apperrors.ErrInvalidRequestBody.Error(), nil) + return + } + + err = h.userService.UpdateUserEmail(ctx, requestBody.Email) + if err != nil { + slog.Error("failed to update user email", "error", err) + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) + return + } + + response.WriteJson(w, http.StatusOK, "email updated successfully", nil) +} + +func (h *handler) SoftDeleteUser(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + userIdValue := ctx.Value(middleware.UserIdKey) + + userId, ok := userIdValue.(int) + if !ok { + slog.Error("error obtaining user id from context") + status, errorMessage := apperrors.MapError(apperrors.ErrContextValue) + response.WriteJson(w, status, errorMessage, nil) + return + } + + err := h.userService.SoftDeleteUser(ctx, userId) + if err != nil { + slog.Error("failed to softdelete user", "error", err) + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) + return + } + + response.WriteJson(w, http.StatusOK, "user scheduled for deletion", nil) +} + +func (h *handler) ListUserRanks(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + leaderboard, err := h.userService.GetAllUsersRank(ctx) + if err != nil { + slog.Error("failed to get all users rank", "error", err) + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) + return + } + + response.WriteJson(w, http.StatusOK, "leaderboard fetched successfully", leaderboard) +} + +func (h *handler) GetCurrentUserRank(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + userIdValue := ctx.Value(middleware.UserIdKey) + + userId, ok := userIdValue.(int) + if !ok { + slog.Error("error obtaining user id from context") + status, errorMessage := apperrors.MapError(apperrors.ErrContextValue) + response.WriteJson(w, status, errorMessage, nil) + return + } + + currentUserRank, err := h.userService.GetCurrentUserRank(ctx, userId) + if err != nil { + slog.Error("failed to get current user rank", "error", err) + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) + return + } + + response.WriteJson(w, http.StatusOK, "current user rank fetched successfully", currentUserRank) +} + +func (h *handler) UpdateCurrentActiveGoalId(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + userIdCtxVal := ctx.Value(middleware.UserIdKey) + userId, ok := userIdCtxVal.(int) + if !ok { + slog.Error("error obtaining user id from context") + status, errorMessage := apperrors.MapError(apperrors.ErrContextValue) + response.WriteJson(w, status, errorMessage, nil) + return + } + + var goal GoalLevel + err := json.NewDecoder(r.Body).Decode(&goal) + if err != nil { + slog.Error(apperrors.ErrFailedMarshal.Error(), "error", err) + response.WriteJson(w, http.StatusBadRequest, apperrors.ErrInvalidRequestBody.Error(), nil) + return + } + + goalId, err := h.userService.UpdateCurrentActiveGoalId(ctx, userId, goal.Level) + if err != nil { + slog.Error("failed to update current active goal id", "error", err) + status, errMsg := apperrors.MapError(err) + response.WriteJson(w, status, errMsg, nil) + return + } + + response.WriteJson(w, http.StatusOK, "Goal updated successfully", goalId) +} diff --git a/backend/internal/app/user/service.go b/backend/internal/app/user/service.go new file mode 100644 index 00000000..2b01e821 --- /dev/null +++ b/backend/internal/app/user/service.go @@ -0,0 +1,182 @@ +package user + +import ( + "context" + "log/slog" + "time" + + "github.com/joshsoftware/code-curiosity-2025/internal/app/goal" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/middleware" + "github.com/joshsoftware/code-curiosity-2025/internal/repository" +) + +type service struct { + userRepository repository.UserRepository + goalService goal.Service +} + +type Service interface { + GetUserById(ctx context.Context, userId int) (User, error) + GetUserByGithubId(ctx context.Context, githubId int) (User, error) + CreateUser(ctx context.Context, userInfo CreateUserRequestBody) (User, error) + UpdateUserEmail(ctx context.Context, email string) error + SoftDeleteUser(ctx context.Context, userId int) error + HardDeleteUsers(ctx context.Context) error + RecoverAccountInGracePeriod(ctx context.Context, userID int) error + UpdateUserCurrentBalance(ctx context.Context, transaction Transaction) error + GetAllUsersRank(ctx context.Context) ([]LeaderboardUser, error) + GetCurrentUserRank(ctx context.Context, userId int) (LeaderboardUser, error) + UpdateCurrentActiveGoalId(ctx context.Context, userId int, level string) (int, error) +} + +func NewService(userRepository repository.UserRepository, goalService goal.Service) Service { + return &service{ + userRepository: userRepository, + goalService: goalService, + } +} + +func (s *service) GetUserById(ctx context.Context, userId int) (User, error) { + userInfo, err := s.userRepository.GetUserById(ctx, nil, userId) + if err != nil { + slog.Error("failed to get user by id", "error", err) + return User{}, err + } + + return User(userInfo), nil + +} + +func (s *service) GetUserByGithubId(ctx context.Context, githubId int) (User, error) { + userInfo, err := s.userRepository.GetUserByGithubId(ctx, nil, githubId) + if err != nil { + slog.Error("failed to get user by github id", "error", err) + return User{}, err + } + + return User(userInfo), nil +} + +func (s *service) CreateUser(ctx context.Context, userInfo CreateUserRequestBody) (User, error) { + user, err := s.userRepository.CreateUser(ctx, nil, repository.CreateUserRequestBody(userInfo)) + if err != nil { + slog.Error("failed to create user", "error", err) + return User{}, apperrors.ErrUserCreationFailed + } + + return User(user), nil +} + +func (s *service) UpdateUserEmail(ctx context.Context, email string) error { + userIdValue := ctx.Value(middleware.UserIdKey) + + userId, ok := userIdValue.(int) + if !ok { + slog.Error("error obtaining user id from context") + return apperrors.ErrInternalServer + } + + err := s.userRepository.UpdateUserEmail(ctx, nil, userId, email) + if err != nil { + slog.Error("failed to update user email", "error", err) + return err + } + + return nil +} + +func (s *service) SoftDeleteUser(ctx context.Context, userID int) error { + now := time.Now() + err := s.userRepository.MarkUserAsDeleted(ctx, nil, userID, now) + if err != nil { + slog.Error("unable to softdelete user", "error", err) + return apperrors.ErrInternalServer + } + return nil +} + +func (s *service) HardDeleteUsers(ctx context.Context) error { + err := s.userRepository.HardDeleteUsers(ctx, nil) + if err != nil { + slog.Error("error deleting users that are soft deleted for more than three months", "error", err) + return err + } + + return nil +} + +func (s *service) RecoverAccountInGracePeriod(ctx context.Context, userID int) error { + err := s.userRepository.RecoverAccountInGracePeriod(ctx, nil, userID) + if err != nil { + slog.Error("failed to recover account in grace period", "error", err) + return err + } + return nil +} + +func (s *service) UpdateUserCurrentBalance(ctx context.Context, transaction Transaction) error { + user, err := s.GetUserById(ctx, transaction.UserId) + if err != nil { + slog.Error("error obtaining user by id", "error", err) + return err + } + + user.CurrentBalance += transaction.TransactedBalance + + tx, ok := middleware.ExtractTxFromContext(ctx) + if !ok { + slog.Error("error obtaining tx from context") + } + + err = s.userRepository.UpdateUserCurrentBalance(ctx, tx, repository.User(user)) + if err != nil { + slog.Error("error updating user current balance", "error", err) + return err + } + + return nil +} + +func (s *service) GetAllUsersRank(ctx context.Context) ([]LeaderboardUser, error) { + userRanks, err := s.userRepository.GetAllUsersRank(ctx, nil) + if err != nil { + slog.Error("error obtaining all users rank", "error", err) + return nil, err + } + + Leaderboard := make([]LeaderboardUser, len(userRanks)) + for i, l := range userRanks { + Leaderboard[i] = LeaderboardUser(l) + } + + return Leaderboard, nil +} + +func (s *service) GetCurrentUserRank(ctx context.Context, userId int) (LeaderboardUser, error) { + currentUserRank, err := s.userRepository.GetCurrentUserRank(ctx, nil, userId) + if err != nil { + slog.Error("error obtaining current user rank", "error", err) + return LeaderboardUser{}, err + } + + return LeaderboardUser(currentUserRank), nil +} + +func (s *service) UpdateCurrentActiveGoalId(ctx context.Context, userId int, level string) (int, error) { + + goalId, err := s.goalService.GetGoalIdByGoalLevel(ctx, level) + + if err != nil { + slog.Error("error occured while fetching goal id by goal level") + return 0, err + } + + goalId, err = s.userRepository.UpdateCurrentActiveGoalId(ctx, nil, userId, goalId) + + if err != nil { + slog.Error("failed to update current active goal id", "error", err) + } + + return goalId, err +} diff --git a/internal/config/app.go b/backend/internal/config/app.go similarity index 65% rename from internal/config/app.go rename to backend/internal/config/app.go index c4715f5c..4070c0f9 100644 --- a/internal/config/app.go +++ b/backend/internal/config/app.go @@ -25,13 +25,19 @@ type GithubOauth struct { RedirectURL string `yaml:"redirect_url" required:"true"` } +type BigqueryProject struct { + ProjectID string `yaml:"project_id" required:"true"` +} + type AppConfig struct { - IsProduction bool `yaml:"is_production"` - HTTPServer HTTPServer `yaml:"http_server"` - Database Database `yaml:"database"` - JWTSecret string `yaml:"jwt_secret"` - ClientURL string `yaml:"client_url"` - GithubOauth GithubOauth `yaml:"github_oauth"` + IsProduction bool `yaml:"is_production"` + HTTPServer HTTPServer `yaml:"http_server"` + Database Database `yaml:"database"` + JWTSecret string `yaml:"jwt_secret"` + ClientURL string `yaml:"client_url"` + GithubOauth GithubOauth `yaml:"github_oauth"` + BigqueryProject BigqueryProject `yaml:"bigquery_project"` + GithubPersonalAccessToken string `yaml:"github_personal_access_token"` } func LoadAppConfig() (AppConfig, error) { diff --git a/backend/internal/config/bigquery.go b/backend/internal/config/bigquery.go new file mode 100644 index 00000000..30294c5d --- /dev/null +++ b/backend/internal/config/bigquery.go @@ -0,0 +1,23 @@ +package config + +import ( + "context" + + "cloud.google.com/go/bigquery" +) + +type Bigquery struct { + Client *bigquery.Client +} + +func BigqueryInit(ctx context.Context, appCfg AppConfig) (Bigquery, error) { + client, err := bigquery.NewClient(ctx, appCfg.BigqueryProject.ProjectID) + if err != nil { + return Bigquery{}, err + } + + bigqueryInstance := Bigquery{ + Client: client, + } + return bigqueryInstance, nil +} diff --git a/internal/config/db.go b/backend/internal/config/db.go similarity index 100% rename from internal/config/db.go rename to backend/internal/config/db.go diff --git a/internal/db/migrate.go b/backend/internal/db/migrate.go similarity index 94% rename from internal/db/migrate.go rename to backend/internal/db/migrate.go index 634e51a7..6b9a159f 100644 --- a/internal/db/migrate.go +++ b/backend/internal/db/migrate.go @@ -42,7 +42,7 @@ func InitMainDBMigrations(config config.AppConfig) (migration Migration, er erro return } -func (migration Migration) MigrationsUpAll(){ +func (migration Migration) MigrationsUpAll() { err := migration.m.Up() if err != nil { if err == migrate.ErrNoChange { @@ -56,7 +56,7 @@ func (migration Migration) MigrationsUpAll(){ slog.Info("Migration up completed") } -func (migration Migration) MigrationsUpWithSteps(steps int){ +func (migration Migration) MigrationsUpWithSteps(steps int) { if err := migration.m.Steps(steps); err != nil { if err == migrate.ErrNoChange { slog.Error("No new migrations to apply") @@ -65,7 +65,7 @@ func (migration Migration) MigrationsUpWithSteps(steps int){ slog.Error("An error occurred while making migrations up", "error", err) return - } + } slog.Info("Current migration version:", "version", migration.MigrationVersion()) slog.Info("Migration up completed") @@ -110,7 +110,7 @@ func (migration Migration) MigrationsDownWithSteps(steps int) { slog.Error("An error occurred while making migrations down", "error", err) return - } + } slog.Info("Current migration version:", "version", migration.MigrationVersion()) slog.Info("Migration down completed") @@ -206,13 +206,17 @@ func main() { } action := os.Args[1] + var steps string + if len(os.Args) > 2 { + steps = os.Args[2] + } switch action { case "up": - migration.MigrationsUp(os.Args[2]) + migration.MigrationsUp(steps) case "down": - migration.MigrationsDown(os.Args[2]) + migration.MigrationsDown(steps) case "create": - migration.CreateMigrationFile(os.Args[2]) + migration.CreateMigrationFile(steps) default: slog.Info("Invalid action. Use 'up' or 'down' or 'create'.") } diff --git a/internal/db/migrations/1748862201_init.down.sql b/backend/internal/db/migrations/1748862201_init.down.sql similarity index 100% rename from internal/db/migrations/1748862201_init.down.sql rename to backend/internal/db/migrations/1748862201_init.down.sql diff --git a/internal/db/migrations/1748862201_init.up.sql b/backend/internal/db/migrations/1748862201_init.up.sql similarity index 92% rename from internal/db/migrations/1748862201_init.up.sql rename to backend/internal/db/migrations/1748862201_init.up.sql index 476693ac..c06fb1d7 100644 --- a/internal/db/migrations/1748862201_init.up.sql +++ b/backend/internal/db/migrations/1748862201_init.up.sql @@ -113,28 +113,28 @@ CREATE TABLE "goal_contribution"( ); ALTER TABLE - "goal_contribution" ADD CONSTRAINT "goal_contribution_set_by_user_id_foreign" FOREIGN KEY("set_by_user_id") REFERENCES "users"("id"); + "goal_contribution" ADD CONSTRAINT "goal_contribution_set_by_user_id_foreign" FOREIGN KEY("set_by_user_id") REFERENCES "users"("id") ON DELETE CASCADE; ALTER TABLE "goal_contribution" ADD CONSTRAINT "goal_contribution_contribution_score_id_foreign" FOREIGN KEY("contribution_score_id") REFERENCES "contribution_score"("id"); ALTER TABLE - "contribution_score" ADD CONSTRAINT "contribution_score_admin_id_foreign" FOREIGN KEY("admin_id") REFERENCES "users"("id"); + "contribution_score" ADD CONSTRAINT "contribution_score_admin_id_foreign" FOREIGN KEY("admin_id") REFERENCES "users"("id") ON DELETE CASCADE; ALTER TABLE - "summary" ADD CONSTRAINT "summary_user_id_foreign" FOREIGN KEY("user_id") REFERENCES "users"("id"); + "summary" ADD CONSTRAINT "summary_user_id_foreign" FOREIGN KEY("user_id") REFERENCES "users"("id") ON DELETE CASCADE; ALTER TABLE - "transactions" ADD CONSTRAINT "transactions_user_id_foreign" FOREIGN KEY("user_id") REFERENCES "users"("id"); + "transactions" ADD CONSTRAINT "transactions_user_id_foreign" FOREIGN KEY("user_id") REFERENCES "users"("id") ON DELETE CASCADE; ALTER TABLE "contributions" ADD CONSTRAINT "contributions_contribution_score_id_foreign" FOREIGN KEY("contribution_score_id") REFERENCES "contribution_score"("id"); ALTER TABLE - "badges" ADD CONSTRAINT "badges_user_id_foreign" FOREIGN KEY("user_id") REFERENCES "users"("id"); + "badges" ADD CONSTRAINT "badges_user_id_foreign" FOREIGN KEY("user_id") REFERENCES "users"("id") ON DELETE CASCADE; ALTER TABLE "goal_contribution" ADD CONSTRAINT "goal_contribution_goal_id_foreign" FOREIGN KEY("goal_id") REFERENCES "goal"("id"); ALTER TABLE "transactions" ADD CONSTRAINT "transactions_contribution_id_foreign" FOREIGN KEY("contribution_id") REFERENCES "contributions"("id"); ALTER TABLE - "contributions" ADD CONSTRAINT "contributions_user_id_foreign" FOREIGN KEY("user_id") REFERENCES "users"("id"); + "contributions" ADD CONSTRAINT "contributions_user_id_foreign" FOREIGN KEY("user_id") REFERENCES "users"("id") ON DELETE CASCADE; ALTER TABLE "contributions" ADD CONSTRAINT "contributions_repository_id_foreign" FOREIGN KEY("repository_id") REFERENCES "repositories"("id"); ALTER TABLE - "leaderboard_hourly" ADD CONSTRAINT "leaderboard_hourly_user_id_foreign" FOREIGN KEY("user_id") REFERENCES "users"("id"); + "leaderboard_hourly" ADD CONSTRAINT "leaderboard_hourly_user_id_foreign" FOREIGN KEY("user_id") REFERENCES "users"("id") ON DELETE CASCADE; ALTER TABLE "summary" ADD CONSTRAINT "summary_contribution_id_foreign" FOREIGN KEY("contribution_id") REFERENCES "contributions"("id"); \ No newline at end of file diff --git a/backend/internal/db/migrations/1750328591_add_column_contributors_url.down.sql b/backend/internal/db/migrations/1750328591_add_column_contributors_url.down.sql new file mode 100644 index 00000000..1ed07319 --- /dev/null +++ b/backend/internal/db/migrations/1750328591_add_column_contributors_url.down.sql @@ -0,0 +1 @@ +ALTER TABLE repositories DROP COLUMN contributors_url; \ No newline at end of file diff --git a/backend/internal/db/migrations/1750328591_add_column_contributors_url.up.sql b/backend/internal/db/migrations/1750328591_add_column_contributors_url.up.sql new file mode 100644 index 00000000..c05df317 --- /dev/null +++ b/backend/internal/db/migrations/1750328591_add_column_contributors_url.up.sql @@ -0,0 +1 @@ +ALTER TABLE repositories ADD COLUMN contributors_url VARCHAR(255) DEFAULT ''; \ No newline at end of file diff --git a/backend/internal/db/migrations/1751016438_allow-null-contribution-id.down.sql b/backend/internal/db/migrations/1751016438_allow-null-contribution-id.down.sql new file mode 100644 index 00000000..ff2f1332 --- /dev/null +++ b/backend/internal/db/migrations/1751016438_allow-null-contribution-id.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE transactions +ALTER COLUMN contribution_id SET NOT NULL; \ No newline at end of file diff --git a/backend/internal/db/migrations/1751016438_allow-null-contribution-id.up.sql b/backend/internal/db/migrations/1751016438_allow-null-contribution-id.up.sql new file mode 100644 index 00000000..8f9e5d0b --- /dev/null +++ b/backend/internal/db/migrations/1751016438_allow-null-contribution-id.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE transactions +ALTER COLUMN contribution_id DROP NOT NULL; diff --git a/backend/internal/db/migrations/1751028730_add-gh-event-id.down.sql b/backend/internal/db/migrations/1751028730_add-gh-event-id.down.sql new file mode 100644 index 00000000..a63e61ff --- /dev/null +++ b/backend/internal/db/migrations/1751028730_add-gh-event-id.down.sql @@ -0,0 +1 @@ +ALTER TABLE contributions DROP COLUMN github_event_id; \ No newline at end of file diff --git a/backend/internal/db/migrations/1751028730_add-gh-event-id.up.sql b/backend/internal/db/migrations/1751028730_add-gh-event-id.up.sql new file mode 100644 index 00000000..334b9764 --- /dev/null +++ b/backend/internal/db/migrations/1751028730_add-gh-event-id.up.sql @@ -0,0 +1 @@ +ALTER TABLE contributions ADD COLUMN github_event_id VARCHAR(255) DEFAULT ''; \ No newline at end of file diff --git a/backend/internal/db/migrations/1751266661_set-not-null-contributors-url.down.sql b/backend/internal/db/migrations/1751266661_set-not-null-contributors-url.down.sql new file mode 100644 index 00000000..7645eb94 --- /dev/null +++ b/backend/internal/db/migrations/1751266661_set-not-null-contributors-url.down.sql @@ -0,0 +1 @@ +ALTER TABLE repositories ALTER COLUMN contributors_url DROP NOT NULL; \ No newline at end of file diff --git a/backend/internal/db/migrations/1751266661_set-not-null-contributors-url.up.sql b/backend/internal/db/migrations/1751266661_set-not-null-contributors-url.up.sql new file mode 100644 index 00000000..bba0f673 --- /dev/null +++ b/backend/internal/db/migrations/1751266661_set-not-null-contributors-url.up.sql @@ -0,0 +1 @@ +ALTER TABLE repositories ALTER COLUMN contributors_url SET NOT NULL; \ No newline at end of file diff --git a/backend/internal/db/migrations/1751268286_set-not-null-gh-event-id.down.sql b/backend/internal/db/migrations/1751268286_set-not-null-gh-event-id.down.sql new file mode 100644 index 00000000..5828c2ec --- /dev/null +++ b/backend/internal/db/migrations/1751268286_set-not-null-gh-event-id.down.sql @@ -0,0 +1 @@ +ALTER TABLE contributions ALTER COLUMN github_event_id DROP NOT NULL; \ No newline at end of file diff --git a/backend/internal/db/migrations/1751268286_set-not-null-gh-event-id.up.sql b/backend/internal/db/migrations/1751268286_set-not-null-gh-event-id.up.sql new file mode 100644 index 00000000..2ac1f91e --- /dev/null +++ b/backend/internal/db/migrations/1751268286_set-not-null-gh-event-id.up.sql @@ -0,0 +1 @@ +ALTER TABLE contributions ALTER COLUMN github_event_id SET NOT NULL; \ No newline at end of file diff --git a/backend/internal/db/migrations/1752476063_create-index-users-current-balance.down.sql b/backend/internal/db/migrations/1752476063_create-index-users-current-balance.down.sql new file mode 100644 index 00000000..a9870134 --- /dev/null +++ b/backend/internal/db/migrations/1752476063_create-index-users-current-balance.down.sql @@ -0,0 +1 @@ +drop index idx_users_current_balance \ No newline at end of file diff --git a/backend/internal/db/migrations/1752476063_create-index-users-current-balance.up.sql b/backend/internal/db/migrations/1752476063_create-index-users-current-balance.up.sql new file mode 100644 index 00000000..a934bf1b --- /dev/null +++ b/backend/internal/db/migrations/1752476063_create-index-users-current-balance.up.sql @@ -0,0 +1 @@ +CREATE INDEX idx_users_current_balance ON users(current_balance DESC); \ No newline at end of file diff --git a/backend/internal/pkg/apperrors/errors.go b/backend/internal/pkg/apperrors/errors.go new file mode 100644 index 00000000..26069af4 --- /dev/null +++ b/backend/internal/pkg/apperrors/errors.go @@ -0,0 +1,77 @@ +package apperrors + +import ( + "errors" + "net/http" +) + +var ( + ErrContextValue = errors.New("error obtaining value from context") + ErrInternalServer = errors.New("internal server error") + + ErrInvalidRequestBody = errors.New("invalid or missing parameters in the request body") + ErrInvalidQueryParams = errors.New("invalid or missing query parameters") + ErrFailedMarshal = errors.New("failed to parse request body") + + ErrUnauthorizedAccess = errors.New("unauthorized. please provide a valid access token") + ErrAccessForbidden = errors.New("access forbidden") + ErrInvalidToken = errors.New("invalid or expired token") + + ErrFailedInitializingLogger = errors.New("failed to initialize logger") + ErrNoAppConfigPath = errors.New("no config path provided") + ErrFailedToLoadAppConfig = errors.New("failed to load environment configuration") + + ErrLoginWithGithubFailed = errors.New("failed to login with Github") + ErrGithubTokenExchangeFailed = errors.New("failed to exchange Github token") + ErrFailedToGetGithubUser = errors.New("failed to get Github user info") + ErrFailedToGetUserEmail = errors.New("failed to get user email from Github") + + ErrUserNotFound = errors.New("user not found") + ErrUserCreationFailed = errors.New("failed to create user") + + ErrJWTCreationFailed = errors.New("failed to create jwt token") + ErrAuthorizationFailed = errors.New("failed to authorize user") + + ErrRepoNotFound = errors.New("repository not found") + ErrRepoCreationFailed = errors.New("failed to create repo for user") + ErrCalculatingUserRepoTotalCoins = errors.New("error calculating total coins earned by user for the repository") + ErrFetchingUsersContributedRepos = errors.New("error fetching users contributed repositories") + ErrFetchingUserContributionsInRepo = errors.New("error fetching users contribution in repository") + + ErrFetchingFromBigquery = errors.New("error fetching contributions from bigquery service") + ErrNextContribution = errors.New("error while loading next bigquery contribution") + ErrContributionCreationFailed = errors.New("failed to create contrbitution") + ErrFetchingRecentContributions = errors.New("failed to fetch users five recent contributions") + ErrFetchingAllContributions = errors.New("failed to fetch all contributions for user") + ErrContributionScoreNotFound = errors.New("failed to get contributionscore details for given contribution type") + ErrFetchingContribution = errors.New("error fetching contribution by github repo id") + ErrContributionNotFound = errors.New("contribution not found") + ErrFetchingContributionTypes = errors.New("failed to fetch all contribution types") + ErrNoContributionForContributionType = errors.New("contribution for contribution type does not exist") + + ErrTransactionCreationFailed = errors.New("error failed to create transaction") + ErrTransactionNotFound = errors.New("error transaction for the contribution id does not exist") + + ErrFetchingGoals = errors.New("error fetching goal levels ") + ErrGoalNotFound = errors.New("goal not found") + ErrCustomGoalTargetCreationFailed = errors.New("failed to create targets for custom goal level") + + ErrBadgeCreationFailed = errors.New("failed to create badge for user") +) + +func MapError(err error) (statusCode int, errMessage string) { + switch err { + case ErrInvalidRequestBody, ErrInvalidQueryParams, ErrContextValue: + return http.StatusBadRequest, err.Error() + case ErrUnauthorizedAccess: + return http.StatusUnauthorized, err.Error() + case ErrAccessForbidden: + return http.StatusForbidden, err.Error() + case ErrUserNotFound, ErrRepoNotFound, ErrContributionNotFound, ErrGoalNotFound: + return http.StatusNotFound, err.Error() + case ErrInvalidToken: + return http.StatusUnprocessableEntity, err.Error() + default: + return http.StatusInternalServerError, ErrInternalServer.Error() + } +} diff --git a/internal/pkg/jwt/jwt.go b/backend/internal/pkg/jwt/jwt.go similarity index 100% rename from internal/pkg/jwt/jwt.go rename to backend/internal/pkg/jwt/jwt.go diff --git a/internal/pkg/middleware/middleware.go b/backend/internal/pkg/middleware/middleware.go similarity index 84% rename from internal/pkg/middleware/middleware.go rename to backend/internal/pkg/middleware/middleware.go index 3ece0897..4c407894 100644 --- a/internal/pkg/middleware/middleware.go +++ b/backend/internal/pkg/middleware/middleware.go @@ -5,12 +5,17 @@ import ( "net/http" "strings" + "github.com/jmoiron/sqlx" "github.com/joshsoftware/code-curiosity-2025/internal/config" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/jwt" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/response" ) +type txKeyType struct{} + +var txKey = txKeyType{} + type contextKey string const ( @@ -18,6 +23,15 @@ const ( IsAdminKey contextKey = "isAdmin" ) +func EmbedTxInContext(ctx context.Context, tx *sqlx.Tx) context.Context { + return context.WithValue(ctx, txKey, tx) +} + +func ExtractTxFromContext(ctx context.Context) (*sqlx.Tx, bool) { + tx, ok := ctx.Value(txKey).(*sqlx.Tx) + return tx, ok +} + func CorsMiddleware(next http.Handler, appCfg config.AppConfig) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", appCfg.ClientURL) diff --git a/internal/pkg/response/response.go b/backend/internal/pkg/response/response.go similarity index 100% rename from internal/pkg/response/response.go rename to backend/internal/pkg/response/response.go diff --git a/backend/internal/pkg/utils/helper.go b/backend/internal/pkg/utils/helper.go new file mode 100644 index 00000000..e324c8dd --- /dev/null +++ b/backend/internal/pkg/utils/helper.go @@ -0,0 +1,79 @@ +package utils + +import ( + "fmt" + "io" + "log/slog" + "net/http" + "strconv" + "strings" + "time" + + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" +) + +func FormatIntSliceForQuery(ids []int) string { + strIDs := make([]string, len(ids)) + for i, id := range ids { + strIDs[i] = fmt.Sprintf("%d", id) + } + + return strings.Join(strIDs, ",") +} + +func DoGet(httpClient *http.Client, url string, headers map[string]string) ([]byte, error) { + req, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + slog.Error("failed to create GET request", "error", err) + return nil, err + } + + for key, value := range headers { + req.Header.Add(key, value) + } + + resp, err := httpClient.Do(req) + if err != nil { + slog.Error("failed to send GET request", "error", err) + return nil, err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + slog.Error("error reading body", "error", err) + return nil, err + } + + return body, nil +} + +func ValidateYearQueryParam(yearVal string) (int, error) { + year, err := strconv.Atoi(yearVal) + if err != nil { + slog.Error("error converting year string value to int") + return 0, err + } + + if year < 2025 || year > time.Now().Year() { + slog.Error("invalid year value") + return 0, apperrors.ErrInvalidQueryParams + } + + return year, nil +} + +func ValidateMonthQueryParam(monthVal string) (int, error) { + month, err := strconv.Atoi(monthVal) + if err != nil { + slog.Error("error converting month string value to int") + return 0, err + } + + if month < 0 || month > 12 { + slog.Error("invalid month value") + return 0, apperrors.ErrInvalidQueryParams + } + + return month, nil +} diff --git a/backend/internal/repository/badge.go b/backend/internal/repository/badge.go new file mode 100644 index 00000000..3298c5e6 --- /dev/null +++ b/backend/internal/repository/badge.go @@ -0,0 +1,86 @@ +package repository + +import ( + "context" + "log/slog" + "time" + + "github.com/jmoiron/sqlx" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" +) + +type badgeRepository struct { + BaseRepository +} + +type BadgeRepository interface { + RepositoryTransaction + GetUserCurrentMonthBadge(ctx context.Context, tx *sqlx.Tx, userId int) (Badge, error) + CreateBadge(ctx context.Context, tx *sqlx.Tx, userId int, badgeType string) (Badge, error) + GetBadgeDetailsOfUser(ctx context.Context, tx *sqlx.Tx, userId int) ([]Badge, error) +} + +func NewBadgeRepository(db *sqlx.DB) BadgeRepository { + return &badgeRepository{ + BaseRepository: BaseRepository{db}, + } +} + +const ( + createBadgeQuery = ` + INSERT INTO badges( + user_id, + badge_type, + earned_at + ) + VALUES($1, $2, $3) + RETURNING *` + + getBadgeDetailsOfUserQuery = "SELECT * FROM badges WHERE user_id = $1 ORDER BY earned_at DESC" + + getUserCurrentMonthBadgeQuery = ` + SELECT * FROM badges + WHERE user_id = $1 + AND earned_at >= DATE_TRUNC('month', CURRENT_DATE) + AND earned_at < DATE_TRUNC('month', CURRENT_DATE + INTERVAL '1 month')` +) + +func (br *badgeRepository) GetUserCurrentMonthBadge(ctx context.Context, tx *sqlx.Tx, userId int) (Badge, error) { + executer := br.BaseRepository.initiateQueryExecuter(tx) + + var badge Badge + err := executer.GetContext(ctx, &badge, getUserCurrentMonthBadgeQuery, userId) + if err != nil { + slog.Error("error fetching current month earned badge for user", "error", err) + return Badge{}, apperrors.ErrBadgeCreationFailed + } + + return badge, nil + +} + +func (br *badgeRepository) CreateBadge(ctx context.Context, tx *sqlx.Tx, userId int, badgeType string) (Badge, error) { + executer := br.BaseRepository.initiateQueryExecuter(tx) + + var createdBadge Badge + err := executer.GetContext(ctx, &createdBadge, createBadgeQuery, userId, badgeType, time.Now()) + if err != nil { + slog.Error("error creating badge for user", "error", err) + return Badge{}, apperrors.ErrBadgeCreationFailed + } + + return createdBadge, nil +} + +func (br *badgeRepository) GetBadgeDetailsOfUser(ctx context.Context, tx *sqlx.Tx, userId int) ([]Badge, error) { + executer := br.BaseRepository.initiateQueryExecuter(tx) + + var badges []Badge + + err := executer.SelectContext(ctx, &badges, getBadgeDetailsOfUserQuery, userId) + if err != nil { + return nil, err + } + + return badges, nil +} diff --git a/internal/repository/base.go b/backend/internal/repository/base.go similarity index 88% rename from internal/repository/base.go rename to backend/internal/repository/base.go index 5516997b..e4b0795e 100644 --- a/internal/repository/base.go +++ b/backend/internal/repository/base.go @@ -23,6 +23,9 @@ type RepositoryTransaction interface { type QueryExecuter interface { QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) + QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error) + GetContext(ctx context.Context, dest interface{}, query string, args ...interface{}) error + SelectContext(ctx context.Context, dest interface{}, query string, args ...interface{}) error } func (b *BaseRepository) BeginTx(ctx context.Context) (*sqlx.Tx, error) { @@ -61,7 +64,7 @@ func (b *BaseRepository) HandleTransaction(ctx context.Context, tx *sqlx.Tx, inc } return nil } - + err := tx.Commit() if err != nil { slog.Error("error occurred while committing database transaction", "error", err) diff --git a/backend/internal/repository/contribution.go b/backend/internal/repository/contribution.go new file mode 100644 index 00000000..7febf7fd --- /dev/null +++ b/backend/internal/repository/contribution.go @@ -0,0 +1,187 @@ +package repository + +import ( + "context" + "database/sql" + "errors" + "log/slog" + + "github.com/jmoiron/sqlx" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/middleware" +) + +type contributionRepository struct { + BaseRepository +} + +type ContributionRepository interface { + RepositoryTransaction + CreateContribution(ctx context.Context, tx *sqlx.Tx, contributionDetails Contribution) (Contribution, error) + GetContributionScoreDetailsByContributionType(ctx context.Context, tx *sqlx.Tx, contributionType string) (ContributionScore, error) + FetchUserContributions(ctx context.Context, tx *sqlx.Tx) ([]Contribution, error) + GetContributionByGithubEventId(ctx context.Context, tx *sqlx.Tx, githubEventId string) (Contribution, error) + GetAllContributionTypes(ctx context.Context, tx *sqlx.Tx) ([]ContributionScore, error) + ListMonthlyContributionSummary(ctx context.Context, tx *sqlx.Tx, year int, month int, userId int) ([]MonthlyContributionSummary, error) + GetContributionTypeByContributionScoreId(ctx context.Context, tx *sqlx.Tx, contributionScoreId int) (string, error) +} + +func NewContributionRepository(db *sqlx.DB) ContributionRepository { + return &contributionRepository{ + BaseRepository: BaseRepository{db}, + } +} + +const ( + createContributionQuery = ` + INSERT INTO contributions ( + user_id, + repository_id, + contribution_score_id, + contribution_type, + balance_change, + contributed_at, + github_event_id + ) + VALUES ($1, $2, $3, $4, $5, $6, $7) + RETURNING *` + + getContributionScoreDetailsByContributionTypeQuery = `SELECT * from contribution_score where contribution_type=$1` + + fetchUserContributionsQuery = `SELECT * from contributions where user_id=$1 order by contributed_at desc` + + getContributionByGithubEventIdQuery = `SELECT * from contributions where github_event_id=$1` + + getAllContributionTypesQuery = `SELECT * from contribution_score` + + getMonthlyContributionSummaryQuery = ` + SELECT + DATE_TRUNC('month', contributed_at) AS month, + contribution_type, + COUNT(*) AS contribution_count, + SUM(balance_change) AS total_coins + FROM contributions + WHERE user_id = $1 + AND DATE_TRUNC('month', contributed_at) = MAKE_DATE($2, $3, 1)::timestamptz + GROUP BY + month, contribution_type;` + + getContributionTypeByContributionScoreIdQuery = `SELECT contribution_type from contribution_score where id=$1` +) + +func (cr *contributionRepository) CreateContribution(ctx context.Context, tx *sqlx.Tx, contributionInfo Contribution) (Contribution, error) { + executer := cr.BaseRepository.initiateQueryExecuter(tx) + + var contribution Contribution + err := executer.GetContext(ctx, &contribution, createContributionQuery, + contributionInfo.UserId, + contributionInfo.RepositoryId, + contributionInfo.ContributionScoreId, + contributionInfo.ContributionType, + contributionInfo.BalanceChange, + contributionInfo.ContributedAt, + contributionInfo.GithubEventId, + ) + if err != nil { + slog.Error("error occured while inserting contributions", "error", err) + return Contribution{}, apperrors.ErrContributionCreationFailed + } + + return contribution, err +} + +func (cr *contributionRepository) GetContributionScoreDetailsByContributionType(ctx context.Context, tx *sqlx.Tx, contributionType string) (ContributionScore, error) { + executer := cr.BaseRepository.initiateQueryExecuter(tx) + + var contributionScoreDetails ContributionScore + err := executer.GetContext(ctx, &contributionScoreDetails, getContributionScoreDetailsByContributionTypeQuery, contributionType) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + slog.Warn("no contribution score details found for contribution type", "contributionType", contributionType) + return ContributionScore{}, apperrors.ErrContributionScoreNotFound + } + + slog.Error("error occured while getting contribution score details", "error", err) + return ContributionScore{}, err + } + + return contributionScoreDetails, nil +} + +func (cr *contributionRepository) FetchUserContributions(ctx context.Context, tx *sqlx.Tx) ([]Contribution, error) { + userIdValue := ctx.Value(middleware.UserIdKey) + + userId, ok := userIdValue.(int) + if !ok { + slog.Error("error obtaining user id from context") + return nil, apperrors.ErrInternalServer + } + + executer := cr.BaseRepository.initiateQueryExecuter(tx) + + var userContributions []Contribution + err := executer.SelectContext(ctx, &userContributions, fetchUserContributionsQuery, userId) + if err != nil { + slog.Error("error fetching user contributions", "error", err) + return nil, apperrors.ErrFetchingAllContributions + } + + return userContributions, nil +} + +func (cr *contributionRepository) GetContributionByGithubEventId(ctx context.Context, tx *sqlx.Tx, githubEventId string) (Contribution, error) { + executer := cr.BaseRepository.initiateQueryExecuter(tx) + + var contribution Contribution + err := executer.GetContext(ctx, &contribution, getContributionByGithubEventIdQuery, githubEventId) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + slog.Error("contribution not found", "error", err) + return Contribution{}, apperrors.ErrContributionNotFound + } + slog.Error("error fetching contribution by github event id", "error", err) + return Contribution{}, apperrors.ErrFetchingContribution + } + + return contribution, nil + +} + +func (cr *contributionRepository) GetAllContributionTypes(ctx context.Context, tx *sqlx.Tx) ([]ContributionScore, error) { + executer := cr.BaseRepository.initiateQueryExecuter(tx) + + var contributionTypes []ContributionScore + err := executer.SelectContext(ctx, &contributionTypes, getAllContributionTypesQuery) + if err != nil { + slog.Error("error fetching all contribution types", "error", err) + return nil, apperrors.ErrFetchingContributionTypes + } + + return contributionTypes, nil +} + +func (cr *contributionRepository) ListMonthlyContributionSummary(ctx context.Context, tx *sqlx.Tx, year int, month int, userId int) ([]MonthlyContributionSummary, error) { + executer := cr.BaseRepository.initiateQueryExecuter(tx) + + var contributionTypeSummary []MonthlyContributionSummary + err := executer.SelectContext(ctx, &contributionTypeSummary, getMonthlyContributionSummaryQuery, userId, year, month) + if err != nil { + slog.Error("error fetching monthly contribution summary for user", "error", err) + return nil, apperrors.ErrInternalServer + } + + return contributionTypeSummary, nil +} + +func (cr *contributionRepository) GetContributionTypeByContributionScoreId(ctx context.Context, tx *sqlx.Tx, contributionScoreId int) (string, error) { + executer := cr.BaseRepository.initiateQueryExecuter(tx) + + var contributionType string + err := executer.GetContext(ctx, &contributionType, getContributionTypeByContributionScoreIdQuery, contributionScoreId) + if err != nil { + slog.Error("error occured while getting contribution type by contribution score id", "error", err) + return contributionType, err + } + + return contributionType, nil +} diff --git a/backend/internal/repository/domain.go b/backend/internal/repository/domain.go new file mode 100644 index 00000000..c3faf145 --- /dev/null +++ b/backend/internal/repository/domain.go @@ -0,0 +1,121 @@ +package repository + +import ( + "database/sql" + "time" +) + +type User struct { + Id int `db:"id"` + GithubId int `db:"github_id"` + GithubUsername string `db:"github_username"` + Email string `db:"email"` + AvatarUrl string `db:"avatar_url"` + CurrentBalance int `db:"current_balance"` + CurrentActiveGoalId sql.NullInt64 `db:"current_active_goal_id"` + IsBlocked bool `db:"is_blocked"` + IsAdmin bool `db:"is_admin"` + Password string `db:"password"` + IsDeleted bool `db:"is_deleted"` + DeletedAt sql.NullTime `db:"deleted_at"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` +} + +type CreateUserRequestBody struct { + GithubId int `db:"github_id"` + GithubUsername string `db:"github_username"` + AvatarUrl string `db:"avatar_url"` + Email string `db:"email"` + IsAdmin bool `db:"is_admin"` +} + +type Contribution struct { + Id int `db:"id"` + UserId int `db:"user_id"` + RepositoryId int `db:"repository_id"` + ContributionScoreId int `db:"contribution_score_id"` + ContributionType string `db:"contribution_type"` + BalanceChange int `db:"balance_change"` + ContributedAt time.Time `db:"contributed_at"` + GithubEventId string `db:"github_event_id"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` +} + +type Repository struct { + Id int `db:"id"` + GithubRepoId int `db:"github_repo_id"` + RepoName string `db:"repo_name"` + Description string `db:"description"` + LanguagesUrl string `db:"languages_url"` + RepoUrl string `db:"repo_url"` + OwnerName string `db:"owner_name"` + UpdateDate time.Time `db:"update_date"` + ContributorsUrl string `db:"contributors_url"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` +} + +type ContributionScore struct { + Id int `db:"id"` + AdminId int `db:"admin_id"` + ContributionType string `db:"contribution_type"` + Score int `db:"score"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` +} + +type Transaction struct { + Id int `db:"id"` + UserId int `db:"user_id"` + ContributionId int `db:"contribution_id"` + IsRedeemed bool `db:"is_redeemed"` + IsGained bool `db:"is_gained"` + TransactedBalance int `db:"transacted_balance"` + TransactedAt time.Time `db:"transacted_at"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` +} + +type LeaderboardUser struct { + Id int `db:"id"` + GithubUsername string `db:"github_username"` + AvatarUrl string `db:"avatar_url"` + CurrentBalance int `db:"current_balance"` + Rank int `db:"rank"` +} + +type MonthlyContributionSummary struct { + Type string `db:"contribution_type"` + Count int `db:"contribution_count"` + TotalCoins int `db:"total_coins"` + Month time.Time `db:"month"` +} + +type Goal struct { + Id int `db:"id"` + Level string `db:"level"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` +} + +type GoalContribution struct { + Id int `db:"id"` + GoalId int `db:"goal_id"` + ContributionScoreId int `db:"contribution_score_id"` + TargetCount int `db:"target_count"` + IsCustom bool `db:"is_custom"` + SetByUserId int `db:"set_by_user_id"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` +} + +type Badge struct { + Id int `db:"id"` + UserId int `db:"user_id"` + BadgeType string `db:"badge_type"` + EarnedAt time.Time `db:"earned_at"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` +} diff --git a/backend/internal/repository/goal.go b/backend/internal/repository/goal.go new file mode 100644 index 00000000..f56bc5e2 --- /dev/null +++ b/backend/internal/repository/goal.go @@ -0,0 +1,139 @@ +package repository + +import ( + "context" + "database/sql" + "errors" + "log/slog" + + "github.com/jmoiron/sqlx" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" +) + +type goalRepository struct { + BaseRepository +} + +type GoalRepository interface { + RepositoryTransaction + ListGoalLevels(ctx context.Context, tx *sqlx.Tx) ([]Goal, error) + GetGoalIdByGoalLevel(ctx context.Context, tx *sqlx.Tx, level string) (int, error) + ListUserGoalLevelTargets(ctx context.Context, tx *sqlx.Tx, userId int) ([]GoalContribution, error) + CreateCustomGoalLevelTarget(ctx context.Context, tx *sqlx.Tx, customGoalContributionInfo GoalContribution) (GoalContribution, error) + GetUserActiveGoalLevel(ctx context.Context, tx *sqlx.Tx, userId int) (string, error) +} + +func NewGoalRepository(db *sqlx.DB) GoalRepository { + return &goalRepository{ + BaseRepository: BaseRepository{db}, + } +} + +const ( + listGoalLevelQuery = "SELECT * from goal;" + + getGoalIdByGoalLevelQuery = "SELECT id from goal where level=$1" + + listUserGoalLevelTargetsQuery = ` + SELECT * from goal_contribution + where goal_id + IN + (SELECT current_active_goal_id from users where id=$1)` + + createCustomGoalLevelTargetQuery = ` + INSERT INTO goal_contribution( + goal_id, + contribution_score_id, + target_count, + is_custom, + set_by_user_id + ) + VALUES + ($1, $2, $3, $4, $5) + RETURNING *` + + getUserActiveGoalLevelQuery = ` + SELECT level from goal + where id IN + (SELECT current_active_goal_id from users where id=$1)` +) + +func (gr *goalRepository) ListGoalLevels(ctx context.Context, tx *sqlx.Tx) ([]Goal, error) { + executer := gr.BaseRepository.initiateQueryExecuter(tx) + + var goals []Goal + err := executer.SelectContext(ctx, &goals, listGoalLevelQuery) + if err != nil { + slog.Error("error fetching goal levels", "error", err) + return nil, apperrors.ErrFetchingGoals + } + + return goals, nil +} + +func (gr *goalRepository) GetGoalIdByGoalLevel(ctx context.Context, tx *sqlx.Tx, level string) (int, error) { + executer := gr.BaseRepository.initiateQueryExecuter(tx) + + var goalId int + err := executer.GetContext(ctx, &goalId, getGoalIdByGoalLevelQuery, level) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + slog.Error("error goal not found", "error", err) + return 0, apperrors.ErrGoalNotFound + } + + slog.Error("error occured while getting goal id by goal level", "error", err) + return 0, apperrors.ErrInternalServer + } + + return goalId, nil +} + +func (gr *goalRepository) ListUserGoalLevelTargets(ctx context.Context, tx *sqlx.Tx, userId int) ([]GoalContribution, error) { + executer := gr.BaseRepository.initiateQueryExecuter(tx) + + var goalLevelTargets []GoalContribution + err := executer.SelectContext(ctx, &goalLevelTargets, listUserGoalLevelTargetsQuery, userId) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + slog.Error("error goal not found", "error", err) + return nil, apperrors.ErrInternalServer + } + + slog.Error("error occured while getting goal id by goal level", "error", err) + return nil, apperrors.ErrInternalServer + } + + return goalLevelTargets, nil +} + +func (gr *goalRepository) CreateCustomGoalLevelTarget(ctx context.Context, tx *sqlx.Tx, customGoalContributionInfo GoalContribution) (GoalContribution, error) { + executer := gr.BaseRepository.initiateQueryExecuter(tx) + + var customGoalContribution GoalContribution + err := executer.GetContext(ctx, &customGoalContribution, createCustomGoalLevelTargetQuery, + customGoalContributionInfo.GoalId, + customGoalContributionInfo.ContributionScoreId, + customGoalContributionInfo.TargetCount, + true, + customGoalContributionInfo.SetByUserId) + if err != nil { + slog.Error("error creating custom goal level targets", "error", err) + return GoalContribution{}, apperrors.ErrCustomGoalTargetCreationFailed + } + + return customGoalContribution, nil +} + +func (gr *goalRepository) GetUserActiveGoalLevel(ctx context.Context, tx *sqlx.Tx, userId int) (string, error) { + executer := gr.BaseRepository.initiateQueryExecuter(tx) + + var userActiveGoalLevel string + err := executer.GetContext(ctx, &userActiveGoalLevel, getUserActiveGoalLevelQuery, userId) + if err != nil { + slog.Error("error getting users current active goal level name", "error", err) + return userActiveGoalLevel, apperrors.ErrInternalServer + } + + return userActiveGoalLevel, nil +} diff --git a/backend/internal/repository/repository.go b/backend/internal/repository/repository.go new file mode 100644 index 00000000..96a1d91f --- /dev/null +++ b/backend/internal/repository/repository.go @@ -0,0 +1,180 @@ +package repository + +import ( + "context" + "database/sql" + "errors" + "log/slog" + + "github.com/jmoiron/sqlx" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/middleware" +) + +type repositoryRepository struct { + BaseRepository +} + +type RepositoryRepository interface { + RepositoryTransaction + GetRepoByGithubId(ctx context.Context, tx *sqlx.Tx, repoGithubId int) (Repository, error) + GetRepoByRepoId(ctx context.Context, tx *sqlx.Tx, repoId int) (Repository, error) + CreateRepository(ctx context.Context, tx *sqlx.Tx, repository Repository) (Repository, error) + GetUserRepoTotalCoins(ctx context.Context, tx *sqlx.Tx, repoId int) (int, error) + FetchUsersContributedRepos(ctx context.Context, tx *sqlx.Tx) ([]Repository, error) + FetchUserContributionsInRepo(ctx context.Context, tx *sqlx.Tx, repoGithubId int) ([]Contribution, error) +} + +func NewRepositoryRepository(db *sqlx.DB) RepositoryRepository { + return &repositoryRepository{ + BaseRepository: BaseRepository{db}, + } +} + +const ( + getRepoByGithubIdQuery = `SELECT * from repositories where github_repo_id=$1` + + getrepoByRepoIdQuery = `SELECT * from repositories where id=$1` + + createRepositoryQuery = ` + INSERT INTO repositories ( + github_repo_id, + repo_name, + description, + languages_url, + repo_url, + owner_name, + update_date, + contributors_url + ) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) + RETURNING *` + + getUserRepoTotalCoinsQuery = `SELECT sum(balance_change) from contributions where user_id = $1 and repository_id = $2;` + + fetchUsersContributedReposQuery = `SELECT * from repositories where id in (SELECT repository_id from contributions where user_id=$1);` + + fetchUserContributionsInRepoQuery = `SELECT * from contributions where repository_id=$1 and user_id=$2;` +) + +func (rr *repositoryRepository) GetRepoByGithubId(ctx context.Context, tx *sqlx.Tx, repoGithubId int) (Repository, error) { + executer := rr.BaseRepository.initiateQueryExecuter(tx) + + var repository Repository + err := executer.GetContext(ctx, &repository, getRepoByGithubIdQuery, repoGithubId) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + slog.Error("repository not found", "error", err) + return Repository{}, apperrors.ErrRepoNotFound + } + slog.Error("error occurred while getting repository by repo github id", "error", err) + return Repository{}, apperrors.ErrInternalServer + } + + return repository, nil + +} + +func (rr *repositoryRepository) GetRepoByRepoId(ctx context.Context, tx *sqlx.Tx, repoId int) (Repository, error) { + executer := rr.BaseRepository.initiateQueryExecuter(tx) + + var repository Repository + err := executer.GetContext(ctx, &repository, getrepoByRepoIdQuery, repoId) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + slog.Error("repository not found", "error", err) + return Repository{}, apperrors.ErrRepoNotFound + } + slog.Error("error occurred while getting repository by id", "error", err) + return Repository{}, apperrors.ErrInternalServer + } + + return repository, nil +} + +func (rr *repositoryRepository) CreateRepository(ctx context.Context, tx *sqlx.Tx, repositoryInfo Repository) (Repository, error) { + executer := rr.BaseRepository.initiateQueryExecuter(tx) + + var repository Repository + err := executer.GetContext(ctx, &repository, createRepositoryQuery, + repositoryInfo.GithubRepoId, + repositoryInfo.RepoName, + repositoryInfo.Description, + repositoryInfo.LanguagesUrl, + repositoryInfo.RepoUrl, + repositoryInfo.OwnerName, + repositoryInfo.UpdateDate, + repositoryInfo.ContributorsUrl, + ) + if err != nil { + slog.Error("error occured while creating repository", "error", err) + return Repository{}, apperrors.ErrInternalServer + } + + return repository, nil + +} + +func (r *repositoryRepository) GetUserRepoTotalCoins(ctx context.Context, tx *sqlx.Tx, repoId int) (int, error) { + userIdValue := ctx.Value(middleware.UserIdKey) + + userId, ok := userIdValue.(int) + if !ok { + slog.Error("error obtaining user id from context") + return 0, apperrors.ErrInternalServer + } + + executer := r.BaseRepository.initiateQueryExecuter(tx) + + var totalCoins int + + err := executer.GetContext(ctx, &totalCoins, getUserRepoTotalCoinsQuery, userId, repoId) + if err != nil { + slog.Error("error calculating total coins earned by user for the repository", "error", err) + return 0, apperrors.ErrCalculatingUserRepoTotalCoins + } + + return totalCoins, nil +} + +func (r *repositoryRepository) FetchUsersContributedRepos(ctx context.Context, tx *sqlx.Tx) ([]Repository, error) { + userIdValue := ctx.Value(middleware.UserIdKey) + + userId, ok := userIdValue.(int) + if !ok { + slog.Error("error obtaining user id from context") + return nil, apperrors.ErrInternalServer + } + + executer := r.BaseRepository.initiateQueryExecuter(tx) + + var usersContributedRepos []Repository + err := executer.SelectContext(ctx, &usersContributedRepos, fetchUsersContributedReposQuery, userId) + if err != nil { + slog.Error("error fetching users contributed repositories", "error", err) + return nil, apperrors.ErrFetchingUsersContributedRepos + } + + return usersContributedRepos, nil +} + +func (r *repositoryRepository) FetchUserContributionsInRepo(ctx context.Context, tx *sqlx.Tx, repoGithubId int) ([]Contribution, error) { + userIdValue := ctx.Value(middleware.UserIdKey) + + userId, ok := userIdValue.(int) + if !ok { + slog.Error("error obtaining user id from context") + return nil, apperrors.ErrInternalServer + } + + executer := r.BaseRepository.initiateQueryExecuter(tx) + + var userContributionsInRepo []Contribution + err := executer.SelectContext(ctx, &userContributionsInRepo, fetchUserContributionsInRepoQuery, repoGithubId, userId) + if err != nil { + slog.Error("error fetching users contribution in repository", "error", err) + return nil, apperrors.ErrFetchingUserContributionsInRepo + } + + return userContributionsInRepo, nil +} diff --git a/backend/internal/repository/transaction.go b/backend/internal/repository/transaction.go new file mode 100644 index 00000000..b1545654 --- /dev/null +++ b/backend/internal/repository/transaction.go @@ -0,0 +1,79 @@ +package repository + +import ( + "context" + "database/sql" + "errors" + "log/slog" + + "github.com/jmoiron/sqlx" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" +) + +type transactionRepository struct { + BaseRepository +} + +type TransactionRepository interface { + RepositoryTransaction + CreateTransaction(ctx context.Context, tx *sqlx.Tx, transactionInfo Transaction) (Transaction, error) + GetTransactionByContributionId(ctx context.Context, tx *sqlx.Tx, contributionId int) (Transaction, error) +} + +func NewTransactionRepository(db *sqlx.DB) TransactionRepository { + return &transactionRepository{ + BaseRepository: BaseRepository{db}, + } +} + +const ( + createTransactionQuery = `INSERT INTO transactions ( + user_id, + contribution_id, + is_redeemed, + is_gained, + transacted_balance, + transacted_at + ) + VALUES ($1, $2, $3, $4, $5, $6) + RETURNING *` + + getTransactionByContributionIdQuery = `SELECT * from transactions where contribution_id=$1` +) + +func (tr *transactionRepository) CreateTransaction(ctx context.Context, tx *sqlx.Tx, transactionInfo Transaction) (Transaction, error) { + executer := tr.BaseRepository.initiateQueryExecuter(tx) + + var transaction Transaction + err := executer.GetContext(ctx, &transaction, createTransactionQuery, + transactionInfo.UserId, + transactionInfo.ContributionId, + transactionInfo.IsRedeemed, + transactionInfo.IsGained, + transactionInfo.TransactedBalance, + transactionInfo.TransactedAt, + ) + if err != nil { + slog.Error("error occured while creating transaction", "error", err) + return Transaction{}, apperrors.ErrTransactionCreationFailed + } + + return transaction, nil +} + +func (tr *transactionRepository) GetTransactionByContributionId(ctx context.Context, tx *sqlx.Tx, contributionId int) (Transaction, error) { + executer := tr.BaseRepository.initiateQueryExecuter(tx) + + var transaction Transaction + err := executer.GetContext(ctx, &transaction, getTransactionByContributionIdQuery, contributionId) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + slog.Error("transaction for the contribution id does not exist", "error", err) + return Transaction{}, apperrors.ErrTransactionNotFound + } + slog.Error("error fetching transaction using contributionid", "error", err) + return Transaction{}, err + } + + return transaction, nil +} diff --git a/backend/internal/repository/user.go b/backend/internal/repository/user.go new file mode 100644 index 00000000..15b1f6a1 --- /dev/null +++ b/backend/internal/repository/user.go @@ -0,0 +1,260 @@ +package repository + +import ( + "context" + "database/sql" + "errors" + "log/slog" + "time" + + "github.com/jmoiron/sqlx" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" +) + +type userRepository struct { + BaseRepository +} + +type UserRepository interface { + RepositoryTransaction + GetUserById(ctx context.Context, tx *sqlx.Tx, userId int) (User, error) + GetUserByGithubId(ctx context.Context, tx *sqlx.Tx, githubId int) (User, error) + CreateUser(ctx context.Context, tx *sqlx.Tx, userInfo CreateUserRequestBody) (User, error) + UpdateUserEmail(ctx context.Context, tx *sqlx.Tx, userId int, email string) error + MarkUserAsDeleted(ctx context.Context, tx *sqlx.Tx, userID int, deletedAt time.Time) error + RecoverAccountInGracePeriod(ctx context.Context, tx *sqlx.Tx, userID int) error + HardDeleteUsers(ctx context.Context, tx *sqlx.Tx) error + GetAllUsersGithubId(ctx context.Context, tx *sqlx.Tx) ([]int, error) + UpdateUserCurrentBalance(ctx context.Context, tx *sqlx.Tx, user User) error + GetAllUsersRank(ctx context.Context, tx *sqlx.Tx) ([]LeaderboardUser, error) + GetCurrentUserRank(ctx context.Context, tx *sqlx.Tx, userId int) (LeaderboardUser, error) + UpdateCurrentActiveGoalId(ctx context.Context, tx *sqlx.Tx, userId int, goalId int) (int, error) +} + +func NewUserRepository(db *sqlx.DB) UserRepository { + return &userRepository{ + BaseRepository: BaseRepository{db}, + } +} + +const ( + getUserByIdQuery = "SELECT * from users where id=$1" + + getUserByGithubIdQuery = "SELECT * from users where github_id=$1" + + createUserQuery = ` + INSERT INTO users ( + github_id, + github_username, + email, + avatar_url + ) + VALUES ($1, $2, $3, $4) + RETURNING *` + + updateEmailQuery = "UPDATE users SET email=$1, updated_at=$2 where id=$3" + + markUserAsDeletedQuery = "UPDATE users SET is_deleted = TRUE, deleted_at=$1 where id = $2" + + recoverAccountInGracePeriodQuery = "UPDATE users SET is_deleted = false, deleted_at = NULL where id = $1" + + hardDeleteUsersQuery = "DELETE FROM users WHERE is_deleted = TRUE AND deleted_at <= $1" + + getAllUsersGithubIdQuery = "SELECT github_id from users" + + updateUserCurrentBalanceQuery = "UPDATE users SET current_balance=$1, updated_at=$2 where id=$3" + + getAllUsersRankQuery = ` + SELECT + id, + github_username, + avatar_url, + current_balance, + RANK() over (ORDER BY current_balance DESC) AS rank + FROM users + ORDER BY current_balance DESC` + + getCurrentUserRankQuery = ` + SELECT * + FROM + ( + SELECT + id, + github_username, + avatar_url, + current_balance, + RANK() OVER (ORDER BY current_balance DESC) AS rank + FROM users + ) + ranked_users + WHERE id = $1;` + + updateCurrentActiveGoalIdQuery = "UPDATE users SET current_active_goal_id=$1 where id=$2" +) + +func (ur *userRepository) GetUserById(ctx context.Context, tx *sqlx.Tx, userId int) (User, error) { + executer := ur.BaseRepository.initiateQueryExecuter(tx) + + var user User + err := executer.GetContext(ctx, &user, getUserByIdQuery, userId) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + slog.Error("user not found", "error", err) + return User{}, apperrors.ErrUserNotFound + } + slog.Error("error occurred while getting user by id", "error", err) + return User{}, apperrors.ErrInternalServer + } + + return user, nil +} + +func (ur *userRepository) GetUserByGithubId(ctx context.Context, tx *sqlx.Tx, githubId int) (User, error) { + executer := ur.BaseRepository.initiateQueryExecuter(tx) + + var user User + err := executer.GetContext(ctx, &user, getUserByGithubIdQuery, githubId) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + slog.Error("user not found", "error", err) + return User{}, apperrors.ErrUserNotFound + } + slog.Error("error occurred while getting user by github id", "error", err) + return User{}, apperrors.ErrInternalServer + } + + return user, nil +} + +func (ur *userRepository) CreateUser(ctx context.Context, tx *sqlx.Tx, userInfo CreateUserRequestBody) (User, error) { + executer := ur.BaseRepository.initiateQueryExecuter(tx) + + var user User + err := executer.GetContext(ctx, &user, createUserQuery, + userInfo.GithubId, + userInfo.GithubUsername, + userInfo.Email, + userInfo.AvatarUrl) + + if err != nil { + slog.Error("error occurred while creating user", "error", err) + return User{}, apperrors.ErrUserCreationFailed + } + + return user, nil + +} + +func (ur *userRepository) UpdateUserEmail(ctx context.Context, tx *sqlx.Tx, userId int, email string) error { + executer := ur.BaseRepository.initiateQueryExecuter(tx) + + _, err := executer.ExecContext(ctx, updateEmailQuery, email, time.Now(), userId) + if err != nil { + slog.Error("failed to update user email", "error", err) + return apperrors.ErrInternalServer + } + + return nil +} + +func (ur *userRepository) MarkUserAsDeleted(ctx context.Context, tx *sqlx.Tx, userID int, deletedAt time.Time) error { + executer := ur.BaseRepository.initiateQueryExecuter(tx) + + _, err := executer.ExecContext(ctx, markUserAsDeletedQuery, deletedAt, userID) + if err != nil { + slog.Error("unable to mark user as deleted", "error", err) + return apperrors.ErrInternalServer + } + + return nil +} + +func (ur *userRepository) RecoverAccountInGracePeriod(ctx context.Context, tx *sqlx.Tx, userID int) error { + executer := ur.BaseRepository.initiateQueryExecuter(tx) + + _, err := executer.ExecContext(ctx, recoverAccountInGracePeriodQuery, userID) + if err != nil { + slog.Error("unable to reverse the soft delete ", "error", err) + return apperrors.ErrInternalServer + } + + return nil +} + +func (ur *userRepository) HardDeleteUsers(ctx context.Context, tx *sqlx.Tx) error { + executer := ur.BaseRepository.initiateQueryExecuter(tx) + + threshold := time.Now().Add(-90 * 1 * time.Second) + + _, err := executer.ExecContext(ctx, hardDeleteUsersQuery, threshold) + if err != nil { + slog.Error("error deleting users that are soft deleted for more than three months", "error", err) + return apperrors.ErrInternalServer + } + + return err +} + +func (ur *userRepository) GetAllUsersGithubId(ctx context.Context, tx *sqlx.Tx) ([]int, error) { + executer := ur.BaseRepository.initiateQueryExecuter(tx) + + var githubIds []int + err := executer.SelectContext(ctx, &githubIds, getAllUsersGithubIdQuery) + if err != nil { + slog.Error("failed to get github usernames", "error", err) + return nil, apperrors.ErrInternalServer + } + + return githubIds, nil +} + +func (ur *userRepository) UpdateUserCurrentBalance(ctx context.Context, tx *sqlx.Tx, user User) error { + executer := ur.BaseRepository.initiateQueryExecuter(tx) + + _, err := executer.ExecContext(ctx, updateUserCurrentBalanceQuery, user.CurrentBalance, time.Now(), user.Id) + if err != nil { + slog.Error("failed to update user balance change", "error", err) + return apperrors.ErrInternalServer + } + + return nil +} + +func (ur *userRepository) GetAllUsersRank(ctx context.Context, tx *sqlx.Tx) ([]LeaderboardUser, error) { + executer := ur.BaseRepository.initiateQueryExecuter(tx) + + var leaderboard []LeaderboardUser + err := executer.SelectContext(ctx, &leaderboard, getAllUsersRankQuery) + if err != nil { + slog.Error("failed to get users rank", "error", err) + return nil, apperrors.ErrInternalServer + } + + return leaderboard, nil +} + +func (ur *userRepository) GetCurrentUserRank(ctx context.Context, tx *sqlx.Tx, userId int) (LeaderboardUser, error) { + + executer := ur.BaseRepository.initiateQueryExecuter(tx) + + var currentUserRank LeaderboardUser + err := executer.GetContext(ctx, ¤tUserRank, getCurrentUserRankQuery, userId) + if err != nil { + slog.Error("failed to get user rank", "error", err) + return LeaderboardUser{}, apperrors.ErrInternalServer + } + + return currentUserRank, nil +} + +func (ur *userRepository) UpdateCurrentActiveGoalId(ctx context.Context, tx *sqlx.Tx, userId int, goalId int) (int, error) { + executer := ur.BaseRepository.initiateQueryExecuter(tx) + + _, err := executer.ExecContext(ctx, updateCurrentActiveGoalIdQuery, goalId, userId) + if err != nil { + slog.Error("failed to update current active goal id", "error", err) + return 0, apperrors.ErrInternalServer + } + + return goalId, nil +} diff --git a/go.mod b/go.mod deleted file mode 100644 index e0eeadaa..00000000 --- a/go.mod +++ /dev/null @@ -1,25 +0,0 @@ -module github.com/joshsoftware/code-curiosity-2025 - -go 1.23.4 - -require ( - github.com/golang-jwt/jwt/v4 v4.5.2 - github.com/ilyakaznacheev/cleanenv v1.5.0 - github.com/jmoiron/sqlx v1.4.0 - github.com/lib/pq v1.10.9 - golang.org/x/oauth2 v0.29.0 -) - -require ( - github.com/BurntSushi/toml v1.2.1 // indirect - github.com/golang-migrate/migrate/v4 v4.18.3 // indirect - github.com/google/go-cmp v0.6.0 // indirect - github.com/hashicorp/errwrap v1.1.0 // indirect - github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/joho/godotenv v1.5.1 // indirect - github.com/kr/pretty v0.3.1 // indirect - go.uber.org/atomic v1.11.0 // indirect - gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect - olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 // indirect -) diff --git a/go.sum b/go.sum deleted file mode 100644 index ddd7ccb7..00000000 --- a/go.sum +++ /dev/null @@ -1,52 +0,0 @@ -filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= -filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= -github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= -github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= -github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= -github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= -github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= -github.com/golang-migrate/migrate/v4 v4.18.3 h1:EYGkoOsvgHHfm5U/naS1RP/6PL/Xv3S4B/swMiAmDLs= -github.com/golang-migrate/migrate/v4 v4.18.3/go.mod h1:99BKpIi6ruaaXRM1A77eqZ+FWPQ3cfRa+ZVy5bmWMaY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= -github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= -github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/ilyakaznacheev/cleanenv v1.5.0 h1:0VNZXggJE2OYdXE87bfSSwGxeiGt9moSR2lOrsHHvr4= -github.com/ilyakaznacheev/cleanenv v1.5.0/go.mod h1:a5aDzaJrLCQZsazHol1w8InnDcOX0OColm64SlIi6gk= -github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= -github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= -github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= -github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -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-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= -github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= -github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= -go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= -go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= -golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98= -golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 h1:slmdOY3vp8a7KQbHkL+FLbvbkgMqmXojpFUO/jENuqQ= -olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3/go.mod h1:oVgVk4OWVDi43qWBEyGhXgYxt7+ED4iYNpTngSLX2Iw= diff --git a/internal/app/dependencies.go b/internal/app/dependencies.go deleted file mode 100644 index fa49370d..00000000 --- a/internal/app/dependencies.go +++ /dev/null @@ -1,35 +0,0 @@ -package app - -import ( - "github.com/jmoiron/sqlx" - "github.com/joshsoftware/code-curiosity-2025/internal/app/auth" - "github.com/joshsoftware/code-curiosity-2025/internal/app/user" - "github.com/joshsoftware/code-curiosity-2025/internal/config" - "github.com/joshsoftware/code-curiosity-2025/internal/repository" -) - -type Dependencies struct { - AuthService auth.Service - UserService user.Service - AuthHandler auth.Handler - UserHandler user.Handler - AppCfg config.AppConfig -} - -func InitDependencies(db *sqlx.DB, appCfg config.AppConfig) Dependencies { - userRepository := repository.NewUserRepository(db) - - userService := user.NewService(userRepository) - authService := auth.NewService(userService, appCfg) - - authHandler := auth.NewHandler(authService, appCfg) - userHandler := user.NewHandler(userService) - - return Dependencies{ - AuthService: authService, - UserService: userService, - AuthHandler: authHandler, - UserHandler: userHandler, - AppCfg: appCfg, - } -} diff --git a/internal/app/router.go b/internal/app/router.go deleted file mode 100644 index 072a53a1..00000000 --- a/internal/app/router.go +++ /dev/null @@ -1,24 +0,0 @@ -package app - -import ( - "net/http" - - "github.com/joshsoftware/code-curiosity-2025/internal/pkg/middleware" - "github.com/joshsoftware/code-curiosity-2025/internal/pkg/response" -) - -func NewRouter(deps Dependencies) http.Handler { - router := http.NewServeMux() - - router.HandleFunc("GET /api/v1/health", func(w http.ResponseWriter, r *http.Request) { - response.WriteJson(w, http.StatusOK, "Server is up and running..", nil) - }) - - router.HandleFunc("GET /api/v1/auth/github", deps.AuthHandler.GithubOAuthLoginUrl) - router.HandleFunc("GET /api/v1/auth/github/callback", deps.AuthHandler.GithubOAuthLoginCallback) - router.HandleFunc("GET /api/v1/auth/user", middleware.Authentication(deps.AuthHandler.GetLoggedInUser, deps.AppCfg)) - - router.HandleFunc("PATCH /api/v1/user/email", middleware.Authentication(deps.UserHandler.UpdateUserEmail, deps.AppCfg)) - - return middleware.CorsMiddleware(router, deps.AppCfg) -} diff --git a/internal/app/user/handler.go b/internal/app/user/handler.go deleted file mode 100644 index 00bcd51e..00000000 --- a/internal/app/user/handler.go +++ /dev/null @@ -1,46 +0,0 @@ -package user - -import ( - "encoding/json" - "log/slog" - "net/http" - - "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" - "github.com/joshsoftware/code-curiosity-2025/internal/pkg/response" -) - -type handler struct { - userService Service -} - -type Handler interface { - UpdateUserEmail(w http.ResponseWriter, r *http.Request) -} - -func NewHandler(userService Service) Handler { - return &handler{ - userService: userService, - } -} - -func (h *handler) UpdateUserEmail(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - - var requestBody Email - err := json.NewDecoder(r.Body).Decode(&requestBody) - if err != nil { - slog.Error(apperrors.ErrFailedMarshal.Error(), "error", err) - response.WriteJson(w, http.StatusBadRequest, apperrors.ErrInvalidRequestBody.Error(), nil) - return - } - - err = h.userService.UpdateUserEmail(ctx, requestBody.Email) - if err != nil { - slog.Error("failed to update user email", "error", err) - status, errorMessage := apperrors.MapError(err) - response.WriteJson(w, status, errorMessage, nil) - return - } - - response.WriteJson(w, http.StatusOK, "email updated successfully", nil) -} diff --git a/internal/app/user/service.go b/internal/app/user/service.go deleted file mode 100644 index 93b85726..00000000 --- a/internal/app/user/service.go +++ /dev/null @@ -1,76 +0,0 @@ -package user - -import ( - "context" - "log/slog" - - "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" - "github.com/joshsoftware/code-curiosity-2025/internal/pkg/middleware" - "github.com/joshsoftware/code-curiosity-2025/internal/repository" -) - -type service struct { - userRepository repository.UserRepository -} - -type Service interface { - GetUserById(ctx context.Context, userId int) (User, error) - GetUserByGithubId(ctx context.Context, githubId int) (User, error) - CreateUser(ctx context.Context, userInfo CreateUserRequestBody) (User, error) - UpdateUserEmail(ctx context.Context, email string) error -} - -func NewService(userRepository repository.UserRepository) Service { - return &service{ - userRepository: userRepository, - } -} - -func (s *service) GetUserById(ctx context.Context, userId int) (User, error) { - userInfo, err := s.userRepository.GetUserById(ctx, nil, userId) - if err != nil { - slog.Error("failed to get user by id", "error", err) - return User{}, err - } - - return User(userInfo), nil - -} - -func (s *service) GetUserByGithubId(ctx context.Context, githubId int) (User, error) { - userInfo, err := s.userRepository.GetUserByGithubId(ctx, nil, githubId) - if err != nil { - slog.Error("failed to get user by github id", "error", err) - return User{}, err - } - - return User(userInfo), nil -} - -func (s *service) CreateUser(ctx context.Context, userInfo CreateUserRequestBody) (User, error) { - user, err := s.userRepository.CreateUser(ctx, nil, repository.CreateUserRequestBody(userInfo)) - if err != nil { - slog.Error("failed to create user", "error", err) - return User{}, apperrors.ErrUserCreationFailed - } - - return User(user), nil -} - -func (s *service) UpdateUserEmail(ctx context.Context, email string) error { - userIdValue := ctx.Value(middleware.UserIdKey) - - userId, ok := userIdValue.(int) - if !ok { - slog.Error("error obtaining user id from context") - return apperrors.ErrInternalServer - } - - err := s.userRepository.UpdateUserEmail(ctx, nil, userId, email) - if err != nil { - slog.Error("failed to update user email", "error", err) - return err - } - - return nil -} diff --git a/internal/pkg/apperrors/errors.go b/internal/pkg/apperrors/errors.go deleted file mode 100644 index 5c7244dd..00000000 --- a/internal/pkg/apperrors/errors.go +++ /dev/null @@ -1,50 +0,0 @@ -package apperrors - -import ( - "errors" - "net/http" -) - -var ( - ErrInternalServer = errors.New("internal server error") - - ErrInvalidRequestBody = errors.New("invalid or missing parameters in the request body") - ErrInvalidQueryParams = errors.New("invalid or missing query parameters") - ErrFailedMarshal = errors.New("failed to parse request body") - - ErrUnauthorizedAccess = errors.New("unauthorized. please provide a valid access token") - ErrAccessForbidden = errors.New("access forbidden") - ErrInvalidToken = errors.New("invalid or expired token") - - ErrFailedInitializingLogger = errors.New("failed to initialize logger") - ErrNoAppConfigPath = errors.New("no config path provided") - ErrFailedToLoadAppConfig = errors.New("failed to load environment configuration") - - ErrLoginWithGithubFailed = errors.New("failed to login with Github") - ErrGithubTokenExchangeFailed = errors.New("failed to exchange Github token") - ErrFailedToGetGithubUser = errors.New("failed to get Github user info") - ErrFailedToGetUserEmail = errors.New("failed to get user email from Github") - - ErrUserNotFound = errors.New("user not found") - ErrUserCreationFailed = errors.New("failed to create user") - - ErrJWTCreationFailed = errors.New("failed to create jwt token") - ErrAuthorizationFailed=errors.New("failed to authorize user") -) - -func MapError(err error) (statusCode int, errMessage string) { - switch err { - case ErrInvalidRequestBody, ErrInvalidQueryParams: - return http.StatusBadRequest, err.Error() - case ErrUnauthorizedAccess: - return http.StatusUnauthorized, err.Error() - case ErrAccessForbidden: - return http.StatusForbidden, err.Error() - case ErrUserNotFound: - return http.StatusNotFound, err.Error() - case ErrInvalidToken: - return http.StatusUnprocessableEntity, err.Error() - default: - return http.StatusInternalServerError, ErrInternalServer.Error() - } -} diff --git a/internal/repository/domain.go b/internal/repository/domain.go deleted file mode 100644 index 8bb35ae4..00000000 --- a/internal/repository/domain.go +++ /dev/null @@ -1,31 +0,0 @@ -package repository - -import ( - "database/sql" - "time" -) - -type User struct { - Id int - GithubId int - GithubUsername string - Email string - AvatarUrl string - CurrentBalance int - CurrentActiveGoalId sql.NullInt64 - IsBlocked bool - IsAdmin bool - Password string - IsDeleted bool - DeletedAt sql.NullTime - CreatedAt time.Time - UpdatedAt time.Time -} - -type CreateUserRequestBody struct { - GithubId int - GithubUsername string - AvatarUrl string - Email string - IsAdmin bool -} diff --git a/internal/repository/user.go b/internal/repository/user.go deleted file mode 100644 index 284ce27e..00000000 --- a/internal/repository/user.go +++ /dev/null @@ -1,158 +0,0 @@ -package repository - -import ( - "context" - "database/sql" - "errors" - "log/slog" - "time" - - "github.com/jmoiron/sqlx" - "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" -) - -type userRepository struct { - BaseRepository -} - -type UserRepository interface { - RepositoryTransaction - GetUserById(ctx context.Context, tx *sqlx.Tx, userId int) (User, error) - GetUserByGithubId(ctx context.Context, tx *sqlx.Tx, githubId int) (User, error) - CreateUser(ctx context.Context, tx *sqlx.Tx, userInfo CreateUserRequestBody) (User, error) - UpdateUserEmail(ctx context.Context, tx *sqlx.Tx, userId int, email string) error -} - -func NewUserRepository(db *sqlx.DB) UserRepository { - return &userRepository{ - BaseRepository: BaseRepository{db}, - } -} - -const ( - getUserByIdQuery = "SELECT * from users where id=$1" - - getUserByGithubIdQuery = "SELECT * from users where github_id=$1" - - createUserQuery = ` - INSERT INTO users ( - github_id, - github_username, - email, - avatar_url - ) - VALUES ($1, $2, $3, $4) - RETURNING *` - - updateEmailQuery = "UPDATE users SET email=$1, updated_at=$2 where id=$3" -) - -func (ur *userRepository) GetUserById(ctx context.Context, tx *sqlx.Tx, userId int) (User, error) { - executer := ur.BaseRepository.initiateQueryExecuter(tx) - - var user User - err := executer.QueryRowContext(ctx, getUserByIdQuery, userId).Scan( - &user.Id, - &user.GithubId, - &user.GithubUsername, - &user.AvatarUrl, - &user.Email, - &user.CurrentActiveGoalId, - &user.CurrentBalance, - &user.IsBlocked, - &user.IsAdmin, - &user.Password, - &user.IsDeleted, - &user.DeletedAt, - &user.CreatedAt, - &user.UpdatedAt, - ) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - slog.Error("user not found", "error", err) - return User{}, apperrors.ErrUserNotFound - } - slog.Error("error occurred while getting user by id", "error", err) - return User{}, apperrors.ErrInternalServer - } - - return user, nil -} - -func (ur *userRepository) GetUserByGithubId(ctx context.Context, tx *sqlx.Tx, githubId int) (User, error) { - executer := ur.BaseRepository.initiateQueryExecuter(tx) - - var user User - err := executer.QueryRowContext(ctx, getUserByGithubIdQuery, githubId).Scan( - &user.Id, - &user.GithubId, - &user.GithubUsername, - &user.AvatarUrl, - &user.Email, - &user.CurrentActiveGoalId, - &user.CurrentBalance, - &user.IsBlocked, - &user.IsAdmin, - &user.Password, - &user.IsDeleted, - &user.DeletedAt, - &user.CreatedAt, - &user.UpdatedAt, - ) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - slog.Error("user not found", "error", err) - return User{}, apperrors.ErrUserNotFound - } - slog.Error("error occurred while getting user by github id", "error", err) - return User{}, apperrors.ErrInternalServer - } - - return user, nil -} - -func (ur *userRepository) CreateUser(ctx context.Context, tx *sqlx.Tx, userInfo CreateUserRequestBody) (User, error) { - executer := ur.BaseRepository.initiateQueryExecuter(tx) - - var user User - err := executer.QueryRowContext(ctx, createUserQuery, - userInfo.GithubId, - userInfo.GithubUsername, - userInfo.Email, - userInfo.AvatarUrl, - ).Scan( - &user.Id, - &user.GithubId, - &user.GithubUsername, - &user.AvatarUrl, - &user.Email, - &user.CurrentActiveGoalId, - &user.CurrentBalance, - &user.IsBlocked, - &user.IsAdmin, - &user.Password, - &user.IsDeleted, - &user.DeletedAt, - &user.CreatedAt, - &user.UpdatedAt, - ) - if err != nil { - slog.Error("error occurred while creating user", "error", err) - return User{}, apperrors.ErrUserCreationFailed - } - - return user, nil - -} - -func (ur *userRepository) UpdateUserEmail(ctx context.Context, tx *sqlx.Tx, userId int, email string) error { - executer := ur.BaseRepository.initiateQueryExecuter(tx) - - _, err := executer.ExecContext(ctx, updateEmailQuery, email, time.Now(), userId) - if err != nil { - slog.Error("failed to update user email", "error", err) - return apperrors.ErrInternalServer - } - - return nil -} From abb482a3dc25cd85f8c1b29e7b5eabfd87920dae Mon Sep 17 00:00:00 2001 From: VishakhaSainani-Josh Date: Mon, 21 Jul 2025 15:06:02 +0530 Subject: [PATCH 02/36] add frontend initial setup --- frontend/.eslintrc.js | 56 + frontend/.gitignore | 24 + frontend/.prettierrc | 16 + frontend/README.md | 69 + frontend/eslint.config.js | 28 + frontend/index.html | 13 + frontend/package-lock.json | 4345 +++++++++++++++++++++++++ frontend/package.json | 48 + frontend/public/vite.svg | 1 + frontend/src/App.css | 42 + frontend/src/App.tsx | 13 + frontend/src/assets/react.svg | 1 + frontend/src/context/AuthProvider.tsx | 46 + frontend/src/index.css | 68 + frontend/src/main.tsx | 10 + frontend/src/pages/Login/main.tsx | 9 + frontend/src/root/PrivateRoutes.tsx | 22 + frontend/src/root/Router.tsx | 22 + frontend/src/root/routeConstants.ts | 4 + frontend/src/root/routesConfig.tsx | 25 + frontend/src/utils/axios.ts | 5 + frontend/src/vite-env.d.ts | 1 + frontend/tsconfig.app.json | 27 + frontend/tsconfig.json | 17 + frontend/tsconfig.node.json | 25 + frontend/vite.config.ts | 12 + frontend/yarn.lock | 1800 ++++++++++ 27 files changed, 6749 insertions(+) create mode 100644 frontend/.eslintrc.js create mode 100644 frontend/.gitignore create mode 100644 frontend/.prettierrc create mode 100644 frontend/README.md create mode 100644 frontend/eslint.config.js create mode 100644 frontend/index.html create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/public/vite.svg create mode 100644 frontend/src/App.css create mode 100644 frontend/src/App.tsx create mode 100644 frontend/src/assets/react.svg create mode 100644 frontend/src/context/AuthProvider.tsx create mode 100644 frontend/src/index.css create mode 100644 frontend/src/main.tsx create mode 100644 frontend/src/pages/Login/main.tsx create mode 100644 frontend/src/root/PrivateRoutes.tsx create mode 100644 frontend/src/root/Router.tsx create mode 100644 frontend/src/root/routeConstants.ts create mode 100644 frontend/src/root/routesConfig.tsx create mode 100644 frontend/src/utils/axios.ts create mode 100644 frontend/src/vite-env.d.ts create mode 100644 frontend/tsconfig.app.json create mode 100644 frontend/tsconfig.json create mode 100644 frontend/tsconfig.node.json create mode 100644 frontend/vite.config.ts create mode 100644 frontend/yarn.lock diff --git a/frontend/.eslintrc.js b/frontend/.eslintrc.js new file mode 100644 index 00000000..74cf8445 --- /dev/null +++ b/frontend/.eslintrc.js @@ -0,0 +1,56 @@ +import globals from "globals"; +import pluginJs from "@eslint/js"; +import tseslint from "typescript-eslint"; +import pluginReact from "eslint-plugin-react"; +import pluginPrettier from "eslint-plugin-prettier"; +import pluginA11y from "eslint-plugin-jsx-a11y"; +import pluginImport from "eslint-plugin-import"; + +/** @type {import('eslint').Linter.FlatConfig[]} */ +export default [ + { + files: ["**/*.{js,mjs,cjs,ts,jsx,tsx}"], + languageOptions: { + globals: globals.browser, + ecmaVersion: "latest", + sourceType: "module", + parser: tseslint.parser + }, + plugins: { + react: pluginReact, + "@typescript-eslint": tseslint.plugin, + prettier: pluginPrettier, + "jsx-a11y": pluginA11y, + import: pluginImport, + }, + rules: { + ...pluginJs.configs.recommended.rules, + ...tseslint.configs.recommended.rules, + ...pluginReact.configs.recommended.rules, + "prettier/prettier": "error", + "react/display-name": "off", + "jsx-a11y/anchor-is-valid": "off", + "jsx-a11y/label-has-for": "off", + "camelcase": "off", + "func-names": ["error", "never"], + "import/prefer-default-export": "off", + "import/no-anonymous-default-export": "off", + "import/no-extraneous-dependencies": "off", + "no-multi-spaces": "off", + "class-methods-use-this": "off", + "no-class-assign": "off", + "key-spacing": "off", + "lines-between-class-members": "off", + "no-param-reassign": "off", + "consistent-return": "off", + "jsx-a11y/href-no-hash": "off", + "import/no-unresolved": "off", + "no-tabs": "off", + "react/react-in-jsx-scope": "off", + "no-use-before-define": "off", + "@typescript-eslint/no-use-before-define": "error", + "react/jsx-filename-extension": ["error", { "extensions": [".tsx"] }], + "react/prop-types": "off" + }, + }, +]; \ No newline at end of file diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 00000000..a547bf36 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend/.prettierrc b/frontend/.prettierrc new file mode 100644 index 00000000..93c3b9e2 --- /dev/null +++ b/frontend/.prettierrc @@ -0,0 +1,16 @@ +{ + "semi": true, + "tabWidth": 2, + "printWidth": 85, + "singleQuote": true, + "trailingComma": "none", + "arrowParens": "avoid", + "endOfLine": "lf", + "htmlWhitespaceSensitivity": "css", + "insertPragma": false, + "jsxSingleQuote": false, + "proseWrap": "always", + "quoteProps": "as-needed", + "requirePragma": false, + "useTabs": false +} \ No newline at end of file diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 00000000..7959ce42 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,69 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default tseslint.config([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + ...tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + ...tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + ...tseslint.configs.stylisticTypeChecked, + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default tseslint.config([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js new file mode 100644 index 00000000..092408a9 --- /dev/null +++ b/frontend/eslint.config.js @@ -0,0 +1,28 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' + +export default tseslint.config( + { ignores: ['dist'] }, + { + extends: [js.configs.recommended, ...tseslint.configs.recommended], + files: ['**/*.{ts,tsx}'], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + plugins: { + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, + }, +) diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 00000000..e4b78eae --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + React + TS + + +
+ + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 00000000..2f392ce8 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,4345 @@ +{ + "name": "code-curiosity-frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "code-curiosity-frontend", + "version": "0.0.0", + "dependencies": { + "axios": "^1.10.0", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "react-router-dom": "^7.7.0", + "react-toastify": "^11.0.5" + }, + "devDependencies": { + "@eslint/js": "^9.29.0", + "@types/react": "^19.1.8", + "@types/react-dom": "^19.1.6", + "@vitejs/plugin-react": "^4.5.2", + "eslint": "^9.31.0", + "eslint-config-prettier": "^10.1.5", + "eslint-plugin-prettier": "^5.5.1", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.20", + "globals": "^16.2.0", + "husky": "^8.0.0", + "lint-staged": "^16.1.2", + "prettier": "^3.6.2", + "typescript": "~5.8.3", + "typescript-eslint": "^8.34.1", + "vite": "^7.0.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", + "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", + "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.0", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.27.3", + "@babel/helpers": "^7.27.6", + "@babel/parser": "^7.28.0", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.0", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz", + "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.0", + "@babel/types": "^7.28.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", + "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz", + "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", + "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz", + "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.0", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.1", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.1.tgz", + "integrity": "sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.6.tgz", + "integrity": "sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.6.tgz", + "integrity": "sha512-S8ToEOVfg++AU/bHwdksHNnyLyVM+eMVAOf6yRKFitnwnbwwPNqKr3srzFRe7nzV69RQKb5DgchIX5pt3L53xg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.6.tgz", + "integrity": "sha512-hd5zdUarsK6strW+3Wxi5qWws+rJhCCbMiC9QZyzoxfk5uHRIE8T287giQxzVpEvCwuJ9Qjg6bEjcRJcgfLqoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.6.tgz", + "integrity": "sha512-0Z7KpHSr3VBIO9A/1wcT3NTy7EB4oNC4upJ5ye3R7taCc2GUdeynSLArnon5G8scPwaU866d3H4BCrE5xLW25A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.6.tgz", + "integrity": "sha512-FFCssz3XBavjxcFxKsGy2DYK5VSvJqa6y5HXljKzhRZ87LvEi13brPrf/wdyl/BbpbMKJNOr1Sd0jtW4Ge1pAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.6.tgz", + "integrity": "sha512-GfXs5kry/TkGM2vKqK2oyiLFygJRqKVhawu3+DOCk7OxLy/6jYkWXhlHwOoTb0WqGnWGAS7sooxbZowy+pK9Yg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.6.tgz", + "integrity": "sha512-aoLF2c3OvDn2XDTRvn8hN6DRzVVpDlj2B/F66clWd/FHLiHaG3aVZjxQX2DYphA5y/evbdGvC6Us13tvyt4pWg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.6.tgz", + "integrity": "sha512-2SkqTjTSo2dYi/jzFbU9Plt1vk0+nNg8YC8rOXXea+iA3hfNJWebKYPs3xnOUf9+ZWhKAaxnQNUf2X9LOpeiMQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.6.tgz", + "integrity": "sha512-SZHQlzvqv4Du5PrKE2faN0qlbsaW/3QQfUUc6yO2EjFcA83xnwm91UbEEVx4ApZ9Z5oG8Bxz4qPE+HFwtVcfyw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.6.tgz", + "integrity": "sha512-b967hU0gqKd9Drsh/UuAm21Khpoh6mPBSgz8mKRq4P5mVK8bpA+hQzmm/ZwGVULSNBzKdZPQBRT3+WuVavcWsQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.6.tgz", + "integrity": "sha512-aHWdQ2AAltRkLPOsKdi3xv0mZ8fUGPdlKEjIEhxCPm5yKEThcUjHpWB1idN74lfXGnZ5SULQSgtr5Qos5B0bPw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.6.tgz", + "integrity": "sha512-VgKCsHdXRSQ7E1+QXGdRPlQ/e08bN6WMQb27/TMfV+vPjjTImuT9PmLXupRlC90S1JeNNW5lzkAEO/McKeJ2yg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.6.tgz", + "integrity": "sha512-WViNlpivRKT9/py3kCmkHnn44GkGXVdXfdc4drNmRl15zVQ2+D2uFwdlGh6IuK5AAnGTo2qPB1Djppj+t78rzw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.6.tgz", + "integrity": "sha512-wyYKZ9NTdmAMb5730I38lBqVu6cKl4ZfYXIs31Baf8aoOtB4xSGi3THmDYt4BTFHk7/EcVixkOV2uZfwU3Q2Jw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.6.tgz", + "integrity": "sha512-KZh7bAGGcrinEj4qzilJ4hqTY3Dg2U82c8bv+e1xqNqZCrCyc+TL9AUEn5WGKDzm3CfC5RODE/qc96OcbIe33w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.6.tgz", + "integrity": "sha512-9N1LsTwAuE9oj6lHMyyAM+ucxGiVnEqUdp4v7IaMmrwb06ZTEVCIs3oPPplVsnjPfyjmxwHxHMF8b6vzUVAUGw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.6.tgz", + "integrity": "sha512-A6bJB41b4lKFWRKNrWoP2LHsjVzNiaurf7wyj/XtFNTsnPuxwEBWHLty+ZE0dWBKuSK1fvKgrKaNjBS7qbFKig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.6.tgz", + "integrity": "sha512-IjA+DcwoVpjEvyxZddDqBY+uJ2Snc6duLpjmkXm/v4xuS3H+3FkLZlDm9ZsAbF9rsfP3zeA0/ArNDORZgrxR/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.6.tgz", + "integrity": "sha512-dUXuZr5WenIDlMHdMkvDc1FAu4xdWixTCRgP7RQLBOkkGgwuuzaGSYcOpW4jFxzpzL1ejb8yF620UxAqnBrR9g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.6.tgz", + "integrity": "sha512-l8ZCvXP0tbTJ3iaqdNf3pjaOSd5ex/e6/omLIQCVBLmHTlfXW3zAxQ4fnDmPLOB1x9xrcSi/xtCWFwCZRIaEwg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.6.tgz", + "integrity": "sha512-hKrmDa0aOFOr71KQ/19JC7az1P0GWtCN1t2ahYAf4O007DHZt/dW8ym5+CUdJhQ/qkZmI1HAF8KkJbEFtCL7gw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.6.tgz", + "integrity": "sha512-+SqBcAWoB1fYKmpWoQP4pGtx+pUUC//RNYhFdbcSA16617cchuryuhOCRpPsjCblKukAckWsV+aQ3UKT/RMPcA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.6.tgz", + "integrity": "sha512-dyCGxv1/Br7MiSC42qinGL8KkG4kX0pEsdb0+TKhmJZgCUDBGmyo1/ArCjNGiOLiIAgdbWgmWgib4HoCi5t7kA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.6.tgz", + "integrity": "sha512-42QOgcZeZOvXfsCBJF5Afw73t4veOId//XD3i+/9gSkhSV6Gk3VPlWncctI+JcOyERv85FUo7RxuxGy+z8A43Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.6.tgz", + "integrity": "sha512-4AWhgXmDuYN7rJI6ORB+uU9DHLq/erBbuMoAuB4VWJTu5KtCgcKYPynF0YI1VkBNuEfjNlLrFr9KZPJzrtLkrQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.6.tgz", + "integrity": "sha512-NgJPHHbEpLQgDH2MjQu90pzW/5vvXIZ7KOnPyNBm92A6WgZ/7b6fJyUBjoumLqeOQQGqY2QjQxRo97ah4Sj0cA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", + "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", + "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.31.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.31.0.tgz", + "integrity": "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.3.tgz", + "integrity": "sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.15.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", + "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", + "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgr/core": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.7.tgz", + "integrity": "sha512-YLT9Zo3oNPJoBjBc4q8G2mjU4tqIbf5CEOORbUUr48dCD9q3umJ3IPlVqOqDakPfd2HuwccBaqlGhN4Gmr5OWg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.45.1.tgz", + "integrity": "sha512-NEySIFvMY0ZQO+utJkgoMiCAjMrGvnbDLHvcmlA33UXJpYBCvlBEbMMtV837uCkS+plG2umfhn0T5mMAxGrlRA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.45.1.tgz", + "integrity": "sha512-ujQ+sMXJkg4LRJaYreaVx7Z/VMgBBd89wGS4qMrdtfUFZ+TSY5Rs9asgjitLwzeIbhwdEhyj29zhst3L1lKsRQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.45.1.tgz", + "integrity": "sha512-FSncqHvqTm3lC6Y13xncsdOYfxGSLnP+73k815EfNmpewPs+EyM49haPS105Rh4aF5mJKywk9X0ogzLXZzN9lA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.45.1.tgz", + "integrity": "sha512-2/vVn/husP5XI7Fsf/RlhDaQJ7x9zjvC81anIVbr4b/f0xtSmXQTFcGIQ/B1cXIYM6h2nAhJkdMHTnD7OtQ9Og==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.45.1.tgz", + "integrity": "sha512-4g1kaDxQItZsrkVTdYQ0bxu4ZIQ32cotoQbmsAnW1jAE4XCMbcBPDirX5fyUzdhVCKgPcrwWuucI8yrVRBw2+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.45.1.tgz", + "integrity": "sha512-L/6JsfiL74i3uK1Ti2ZFSNsp5NMiM4/kbbGEcOCps99aZx3g8SJMO1/9Y0n/qKlWZfn6sScf98lEOUe2mBvW9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.45.1.tgz", + "integrity": "sha512-RkdOTu2jK7brlu+ZwjMIZfdV2sSYHK2qR08FUWcIoqJC2eywHbXr0L8T/pONFwkGukQqERDheaGTeedG+rra6Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.45.1.tgz", + "integrity": "sha512-3kJ8pgfBt6CIIr1o+HQA7OZ9mp/zDk3ctekGl9qn/pRBgrRgfwiffaUmqioUGN9hv0OHv2gxmvdKOkARCtRb8Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.45.1.tgz", + "integrity": "sha512-k3dOKCfIVixWjG7OXTCOmDfJj3vbdhN0QYEqB+OuGArOChek22hn7Uy5A/gTDNAcCy5v2YcXRJ/Qcnm4/ma1xw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.45.1.tgz", + "integrity": "sha512-PmI1vxQetnM58ZmDFl9/Uk2lpBBby6B6rF4muJc65uZbxCs0EA7hhKCk2PKlmZKuyVSHAyIw3+/SiuMLxKxWog==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.45.1.tgz", + "integrity": "sha512-9UmI0VzGmNJ28ibHW2GpE2nF0PBQqsyiS4kcJ5vK+wuwGnV5RlqdczVocDSUfGX/Na7/XINRVoUgJyFIgipoRg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.45.1.tgz", + "integrity": "sha512-7nR2KY8oEOUTD3pBAxIBBbZr0U7U+R9HDTPNy+5nVVHDXI4ikYniH1oxQz9VoB5PbBU1CZuDGHkLJkd3zLMWsg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.45.1.tgz", + "integrity": "sha512-nlcl3jgUultKROfZijKjRQLUu9Ma0PeNv/VFHkZiKbXTBQXhpytS8CIj5/NfBeECZtY2FJQubm6ltIxm/ftxpw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.45.1.tgz", + "integrity": "sha512-HJV65KLS51rW0VY6rvZkiieiBnurSzpzore1bMKAhunQiECPuxsROvyeaot/tcK3A3aGnI+qTHqisrpSgQrpgA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.45.1.tgz", + "integrity": "sha512-NITBOCv3Qqc6hhwFt7jLV78VEO/il4YcBzoMGGNxznLgRQf43VQDae0aAzKiBeEPIxnDrACiMgbqjuihx08OOw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.45.1.tgz", + "integrity": "sha512-+E/lYl6qu1zqgPEnTrs4WysQtvc/Sh4fC2nByfFExqgYrqkKWp1tWIbe+ELhixnenSpBbLXNi6vbEEJ8M7fiHw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.45.1.tgz", + "integrity": "sha512-a6WIAp89p3kpNoYStITT9RbTbTnqarU7D8N8F2CV+4Cl9fwCOZraLVuVFvlpsW0SbIiYtEnhCZBPLoNdRkjQFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.45.1.tgz", + "integrity": "sha512-T5Bi/NS3fQiJeYdGvRpTAP5P02kqSOpqiopwhj0uaXB6nzs5JVi2XMJb18JUSKhCOX8+UE1UKQufyD6Or48dJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.45.1.tgz", + "integrity": "sha512-lxV2Pako3ujjuUe9jiU3/s7KSrDfH6IgTSQOnDWr9aJ92YsFd7EurmClK0ly/t8dzMkDtd04g60WX6yl0sGfdw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.45.1.tgz", + "integrity": "sha512-M/fKi4sasCdM8i0aWJjCSFm2qEnYRR8AMLG2kxp6wD13+tMGA4Z1tVAuHkNRjud5SW2EM3naLuK35w9twvf6aA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", + "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.1.8", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz", + "integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.1.6", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.6.tgz", + "integrity": "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.37.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.37.0.tgz", + "integrity": "sha512-jsuVWeIkb6ggzB+wPCsR4e6loj+rM72ohW6IBn2C+5NCvfUVY8s33iFPySSVXqtm5Hu29Ne/9bnA0JmyLmgenA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.37.0", + "@typescript-eslint/type-utils": "8.37.0", + "@typescript-eslint/utils": "8.37.0", + "@typescript-eslint/visitor-keys": "8.37.0", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.37.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.37.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.37.0.tgz", + "integrity": "sha512-kVIaQE9vrN9RLCQMQ3iyRlVJpTiDUY6woHGb30JDkfJErqrQEmtdWH3gV0PBAfGZgQXoqzXOO0T3K6ioApbbAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.37.0", + "@typescript-eslint/types": "8.37.0", + "@typescript-eslint/typescript-estree": "8.37.0", + "@typescript-eslint/visitor-keys": "8.37.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.37.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.37.0.tgz", + "integrity": "sha512-BIUXYsbkl5A1aJDdYJCBAo8rCEbAvdquQ8AnLb6z5Lp1u3x5PNgSSx9A/zqYc++Xnr/0DVpls8iQ2cJs/izTXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.37.0", + "@typescript-eslint/types": "^8.37.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.37.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.37.0.tgz", + "integrity": "sha512-0vGq0yiU1gbjKob2q691ybTg9JX6ShiVXAAfm2jGf3q0hdP6/BruaFjL/ManAR/lj05AvYCH+5bbVo0VtzmjOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.37.0", + "@typescript-eslint/visitor-keys": "8.37.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.37.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.37.0.tgz", + "integrity": "sha512-1/YHvAVTimMM9mmlPvTec9NP4bobA1RkDbMydxG8omqwJJLEW/Iy2C4adsAESIXU3WGLXFHSZUU+C9EoFWl4Zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.37.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.37.0.tgz", + "integrity": "sha512-SPkXWIkVZxhgwSwVq9rqj/4VFo7MnWwVaRNznfQDc/xPYHjXnPfLWn+4L6FF1cAz6e7dsqBeMawgl7QjUMj4Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.37.0", + "@typescript-eslint/typescript-estree": "8.37.0", + "@typescript-eslint/utils": "8.37.0", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.37.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.37.0.tgz", + "integrity": "sha512-ax0nv7PUF9NOVPs+lmQ7yIE7IQmAf8LGcXbMvHX5Gm+YJUYNAl340XkGnrimxZ0elXyoQJuN5sbg6C4evKA4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.37.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.37.0.tgz", + "integrity": "sha512-zuWDMDuzMRbQOM+bHyU4/slw27bAUEcKSKKs3hcv2aNnc/tvE/h7w60dwVw8vnal2Pub6RT1T7BI8tFZ1fE+yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.37.0", + "@typescript-eslint/tsconfig-utils": "8.37.0", + "@typescript-eslint/types": "8.37.0", + "@typescript-eslint/visitor-keys": "8.37.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.37.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.37.0.tgz", + "integrity": "sha512-TSFvkIW6gGjN2p6zbXo20FzCABbyUAuq6tBvNRGsKdsSQ6a7rnV6ADfZ7f4iI3lIiXc4F4WWvtUfDw9CJ9pO5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.37.0", + "@typescript-eslint/types": "8.37.0", + "@typescript-eslint/typescript-estree": "8.37.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.37.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.37.0.tgz", + "integrity": "sha512-YzfhzcTnZVPiLfP/oeKtDp2evwvHLMe0LOy7oe+hb9KKIumLNohYS9Hgp1ifwpu42YWxhZE8yieggz6JpqO/1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.37.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", + "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz", + "integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.25.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", + "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001726", + "electron-to-chromium": "^1.5.173", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001727", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz", + "integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", + "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.0.tgz", + "integrity": "sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.187", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.187.tgz", + "integrity": "sha512-cl5Jc9I0KGUoOoSbxvTywTa40uspGJt/BDBoDLoxJRSBpWh4FFXBsjNRHfQrONsV/OoEjDfHUmZQa2d6Ze4YgA==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.6.tgz", + "integrity": "sha512-GVuzuUwtdsghE3ocJ9Bs8PNoF13HNQ5TXbEi2AhvVb8xU1Iwt9Fos9FEamfoee+u/TOsn7GUWc04lz46n2bbTg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.6", + "@esbuild/android-arm": "0.25.6", + "@esbuild/android-arm64": "0.25.6", + "@esbuild/android-x64": "0.25.6", + "@esbuild/darwin-arm64": "0.25.6", + "@esbuild/darwin-x64": "0.25.6", + "@esbuild/freebsd-arm64": "0.25.6", + "@esbuild/freebsd-x64": "0.25.6", + "@esbuild/linux-arm": "0.25.6", + "@esbuild/linux-arm64": "0.25.6", + "@esbuild/linux-ia32": "0.25.6", + "@esbuild/linux-loong64": "0.25.6", + "@esbuild/linux-mips64el": "0.25.6", + "@esbuild/linux-ppc64": "0.25.6", + "@esbuild/linux-riscv64": "0.25.6", + "@esbuild/linux-s390x": "0.25.6", + "@esbuild/linux-x64": "0.25.6", + "@esbuild/netbsd-arm64": "0.25.6", + "@esbuild/netbsd-x64": "0.25.6", + "@esbuild/openbsd-arm64": "0.25.6", + "@esbuild/openbsd-x64": "0.25.6", + "@esbuild/openharmony-arm64": "0.25.6", + "@esbuild/sunos-x64": "0.25.6", + "@esbuild/win32-arm64": "0.25.6", + "@esbuild/win32-ia32": "0.25.6", + "@esbuild/win32-x64": "0.25.6" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.31.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.31.0.tgz", + "integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.3.0", + "@eslint/core": "^0.15.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.31.0", + "@eslint/plugin-kit": "^0.3.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.5", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.5.tgz", + "integrity": "sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.1.tgz", + "integrity": "sha512-dobTkHT6XaEVOo8IO90Q4DOSxnm3Y151QxPJlM/vKC0bVy+d6cVWQZLlFiuZPP0wS6vZwSKeJgKkcS+KfMBlRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.11.7" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.20", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.20.tgz", + "integrity": "sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", + "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.3.0.tgz", + "integrity": "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/husky": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/husky/-/husky-8.0.3.tgz", + "integrity": "sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==", + "dev": true, + "license": "MIT", + "bin": { + "husky": "lib/bin.js" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lint-staged": { + "version": "16.1.2", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.1.2.tgz", + "integrity": "sha512-sQKw2Si2g9KUZNY3XNvRuDq4UJqpHwF0/FQzZR2M7I5MvtpWvibikCjUVJzZdGE0ByurEl3KQNvsGetd1ty1/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.4.1", + "commander": "^14.0.0", + "debug": "^4.4.1", + "lilconfig": "^3.1.3", + "listr2": "^8.3.3", + "micromatch": "^4.0.8", + "nano-spawn": "^1.0.2", + "pidtree": "^0.6.0", + "string-argv": "^0.3.2", + "yaml": "^2.8.0" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": ">=20.17" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + } + }, + "node_modules/lint-staged/node_modules/chalk": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/listr2": { + "version": "8.3.3", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.3.3.tgz", + "integrity": "sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^4.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/is-fullwidth-code-point": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz", + "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz", + "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nano-spawn": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-1.0.2.tgz", + "integrity": "sha512-21t+ozMQDAL/UGgQVBbZ/xXvNO10++ZPuTmKRO8k9V3AClVRht49ahtDjfY8l1q6nSHOrE5ASfthzH3ol6R/hg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/nano-spawn?sponsor=1" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" + }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", + "dev": true, + "license": "MIT", + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", + "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", + "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.26.0" + }, + "peerDependencies": { + "react": "^19.1.0" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.7.0.tgz", + "integrity": "sha512-3FUYSwlvB/5wRJVTL/aavqHmfUKe0+Xm9MllkYgGo9eDwNdkvwlJGjpPxono1kCycLt6AnDTgjmXvK3/B4QGuw==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.7.0.tgz", + "integrity": "sha512-wwGS19VkNBkneVh9/YD0pK3IsjWxQUVMDD6drlG7eJpo1rXBtctBqDyBm/k+oKHRAm1x9XWT3JFC82QI9YOXXA==", + "license": "MIT", + "dependencies": { + "react-router": "7.7.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/react-toastify": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-11.0.5.tgz", + "integrity": "sha512-EpqHBGvnSTtHYhCPLxML05NLY2ZX0JURbAdNYa6BUkk+amz4wbKBQvoKQAB0ardvSarUBuY4Q4s1sluAzZwkmA==", + "license": "MIT", + "dependencies": { + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": "^18 || ^19", + "react-dom": "^18 || ^19" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.45.1.tgz", + "integrity": "sha512-4iya7Jb76fVpQyLoiVpzUrsjQ12r3dM7fIVz+4NwoYvZOShknRmiv+iu9CClZml5ZLGb0XMcYLutK6w9tgxHDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.45.1", + "@rollup/rollup-android-arm64": "4.45.1", + "@rollup/rollup-darwin-arm64": "4.45.1", + "@rollup/rollup-darwin-x64": "4.45.1", + "@rollup/rollup-freebsd-arm64": "4.45.1", + "@rollup/rollup-freebsd-x64": "4.45.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.45.1", + "@rollup/rollup-linux-arm-musleabihf": "4.45.1", + "@rollup/rollup-linux-arm64-gnu": "4.45.1", + "@rollup/rollup-linux-arm64-musl": "4.45.1", + "@rollup/rollup-linux-loongarch64-gnu": "4.45.1", + "@rollup/rollup-linux-powerpc64le-gnu": "4.45.1", + "@rollup/rollup-linux-riscv64-gnu": "4.45.1", + "@rollup/rollup-linux-riscv64-musl": "4.45.1", + "@rollup/rollup-linux-s390x-gnu": "4.45.1", + "@rollup/rollup-linux-x64-gnu": "4.45.1", + "@rollup/rollup-linux-x64-musl": "4.45.1", + "@rollup/rollup-win32-arm64-msvc": "4.45.1", + "@rollup/rollup-win32-ia32-msvc": "4.45.1", + "@rollup/rollup-win32-x64-msvc": "4.45.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.19" + } + }, + "node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/synckit": { + "version": "0.11.8", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.8.tgz", + "integrity": "sha512-+XZ+r1XGIJGeQk3VvXhT6xx/VpbHsRzsTkGgF6E5RX9TTXD0118l87puaEBZ566FhqblC6U0d4XnubznJDm30A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.4" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", + "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", + "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.37.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.37.0.tgz", + "integrity": "sha512-TnbEjzkE9EmcO0Q2zM+GE8NQLItNAJpMmED1BdgoBMYNdqMhzlbqfdSwiRlAzEK2pA9UzVW0gzaaIzXWg2BjfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.37.0", + "@typescript-eslint/parser": "8.37.0", + "@typescript-eslint/typescript-estree": "8.37.0", + "@typescript-eslint/utils": "8.37.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.0.5.tgz", + "integrity": "sha512-1mncVwJxy2C9ThLwz0+2GKZyEXuC3MyWtAAlNftlZZXZDP3AJt5FmwcMit/IGGaNZ8ZOB2BNO/HFUB+CpN0NQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.6", + "picomatch": "^4.0.2", + "postcss": "^8.5.6", + "rollup": "^4.40.0", + "tinyglobby": "^0.2.14" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", + "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", + "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 00000000..77e36516 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,48 @@ +{ + "name": "code-curiosity-frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "lint:fix": "eslint --fix . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "prettier": "prettier . --ignore-path .gitignore", + "format:check": "npm run prettier -- --check", + "format:fix": "npm run prettier -- --write" + }, + "dependencies": { + "axios": "^1.10.0", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "react-router-dom": "^7.7.0", + "react-toastify": "^11.0.5" + }, + "devDependencies": { + "@eslint/js": "^9.29.0", + "@types/react": "^19.1.8", + "@types/react-dom": "^19.1.6", + "@vitejs/plugin-react": "^4.5.2", + "eslint": "^9.31.0", + "eslint-config-prettier": "^10.1.5", + "eslint-plugin-prettier": "^5.5.1", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.20", + "globals": "^16.2.0", + "husky": "^8.0.0", + "lint-staged": "^16.1.2", + "prettier": "^3.6.2", + "typescript": "~5.8.3", + "typescript-eslint": "^8.34.1", + "vite": "^7.0.0" + }, + "lint-staged": { + "**/*": "prettier --write --ignore-unknown", + "**/*.{js,jsx,ts,tsx}": [ + "npm run lint" + ] + }, + "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" +} diff --git a/frontend/public/vite.svg b/frontend/public/vite.svg new file mode 100644 index 00000000..e7b8dfb1 --- /dev/null +++ b/frontend/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/App.css b/frontend/src/App.css new file mode 100644 index 00000000..b9d355df --- /dev/null +++ b/frontend/src/App.css @@ -0,0 +1,42 @@ +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 00000000..4e403b28 --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,13 @@ +import "./App.css"; +import { UserProvider } from "./context/AuthProvider"; +import Router from "./root/Router"; + +function App() { + return ( + + + + ); +} + +export default App; diff --git a/frontend/src/assets/react.svg b/frontend/src/assets/react.svg new file mode 100644 index 00000000..6c87de9b --- /dev/null +++ b/frontend/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/context/AuthProvider.tsx b/frontend/src/context/AuthProvider.tsx new file mode 100644 index 00000000..a2165f30 --- /dev/null +++ b/frontend/src/context/AuthProvider.tsx @@ -0,0 +1,46 @@ +import { + createContext, + useMemo, + useState, + type Dispatch, + type ReactNode, + type SetStateAction, +} from "react"; + +export type User = { + githubId: string; + githubUsername: string; + avatarUrl: string; +}; + +export interface UserContextInterface { + user: User; + setUser: Dispatch>; +} + +const defaultState = { + user: { + githubId: "", + githubUsername: "", + avatarUrl: "", + }, + setUser: (user: User) => {}, +} as UserContextInterface; + +export const UserContext = createContext(defaultState); + +type UserProviderProps = { + children: ReactNode; +}; + +export const UserProvider = ({ children }: UserProviderProps) => { + const [user, setUser] = useState(defaultState.user); + + const memoizedUserValue = useMemo(() => ({ user, setUser }), [user, setUser]); + + return ( + + {children} + + ); +}; diff --git a/frontend/src/index.css b/frontend/src/index.css new file mode 100644 index 00000000..08a3ac9e --- /dev/null +++ b/frontend/src/index.css @@ -0,0 +1,68 @@ +:root { + font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx new file mode 100644 index 00000000..bef5202a --- /dev/null +++ b/frontend/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.css' +import App from './App.tsx' + +createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/frontend/src/pages/Login/main.tsx b/frontend/src/pages/Login/main.tsx new file mode 100644 index 00000000..8daae3f3 --- /dev/null +++ b/frontend/src/pages/Login/main.tsx @@ -0,0 +1,9 @@ +import { useContext } from "react"; +import { UserContext } from "../../context/AuthProvider"; + +function Login() { + const { user } = useContext(UserContext); + + return
login page username : {user.githubUsername}
; +} +export default Login; diff --git a/frontend/src/root/PrivateRoutes.tsx b/frontend/src/root/PrivateRoutes.tsx new file mode 100644 index 00000000..27ba9999 --- /dev/null +++ b/frontend/src/root/PrivateRoutes.tsx @@ -0,0 +1,22 @@ +import { useEffect, type ReactNode } from "react"; +import { Navigate } from "react-router-dom"; +import { toast } from "react-toastify"; +import "react-toastify/dist/ReactToastify.css"; + +interface PrivateRoutesProps { + children: ReactNode; +} + +const PrivateRoutes: React.FC = ({ children }) => { + const localStorageToken = localStorage.getItem("token"); + + useEffect(() => { + if (!localStorageToken) { + toast.warning("You need to login to proceed!"); + } + }, [localStorageToken]); + + return localStorageToken ? children : ; +}; + +export default PrivateRoutes; diff --git a/frontend/src/root/Router.tsx b/frontend/src/root/Router.tsx new file mode 100644 index 00000000..944e51d4 --- /dev/null +++ b/frontend/src/root/Router.tsx @@ -0,0 +1,22 @@ +import { createBrowserRouter, RouterProvider } from "react-router-dom"; +import PrivateRoutes from "./PrivateRoutes"; +import { routesConfig, type RoutesType } from "./routesConfig"; + +const generateRoutes = (routes: RoutesType[]) => { + return routes.map(({ path, element, isProtected }) => { + let wrappedElement = element; + + if (isProtected) { + wrappedElement = {wrappedElement}; + } + + return { path, element: wrappedElement }; + }); +}; + +const Router = () => { + const router = createBrowserRouter(generateRoutes(routesConfig)); + return ; +}; + +export default Router; diff --git a/frontend/src/root/routeConstants.ts b/frontend/src/root/routeConstants.ts new file mode 100644 index 00000000..96e0b22e --- /dev/null +++ b/frontend/src/root/routeConstants.ts @@ -0,0 +1,4 @@ +export const ROUTES = { + LOGIN: "/login", + LANDING: "/", +}; diff --git a/frontend/src/root/routesConfig.tsx b/frontend/src/root/routesConfig.tsx new file mode 100644 index 00000000..674aaf1a --- /dev/null +++ b/frontend/src/root/routesConfig.tsx @@ -0,0 +1,25 @@ +import type { ReactNode } from "react"; +import Login from "../pages/Login/main"; +import { ROUTES } from "./routeConstants"; + +export interface RoutesType { + path: string; + element: ReactNode; + isProtected: boolean; +} + +export const routesConfig: RoutesType[] = [ + { + path: ROUTES.LOGIN, + element: , + isProtected: false, + }, + // { + // path: //path, + // element: ( + // + // {/* protected component */} + // + // ), + // }, +]; diff --git a/frontend/src/utils/axios.ts b/frontend/src/utils/axios.ts new file mode 100644 index 00000000..fdfb3f57 --- /dev/null +++ b/frontend/src/utils/axios.ts @@ -0,0 +1,5 @@ +import axios from "axios"; + +export const api = axios.create({ + baseURL: "http://localhost:8080/api/v1", +}); diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts new file mode 100644 index 00000000..11f02fe2 --- /dev/null +++ b/frontend/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json new file mode 100644 index 00000000..227a6c67 --- /dev/null +++ b/frontend/tsconfig.app.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 00000000..1e173931 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,17 @@ +{ + "files": [], + "references": [ + { + "path": "./tsconfig.app.json" + }, + { + "path": "./tsconfig.node.json" + } + ], + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + } +} diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json new file mode 100644 index 00000000..f85a3990 --- /dev/null +++ b/frontend/tsconfig.node.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 00000000..db008821 --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], + resolve: { + alias: { + '@': '/src' + } + } +}); diff --git a/frontend/yarn.lock b/frontend/yarn.lock new file mode 100644 index 00000000..eacee312 --- /dev/null +++ b/frontend/yarn.lock @@ -0,0 +1,1800 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@ampproject/remapping@^2.2.0": + version "2.3.0" + resolved "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz" + integrity sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw== + dependencies: + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.24" + +"@babel/code-frame@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz" + integrity sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg== + dependencies: + "@babel/helper-validator-identifier" "^7.27.1" + js-tokens "^4.0.0" + picocolors "^1.1.1" + +"@babel/compat-data@^7.27.2": + version "7.28.0" + resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz" + integrity sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw== + +"@babel/core@^7.0.0", "@babel/core@^7.0.0-0", "@babel/core@^7.28.0": + version "7.28.0" + resolved "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz" + integrity sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ== + dependencies: + "@ampproject/remapping" "^2.2.0" + "@babel/code-frame" "^7.27.1" + "@babel/generator" "^7.28.0" + "@babel/helper-compilation-targets" "^7.27.2" + "@babel/helper-module-transforms" "^7.27.3" + "@babel/helpers" "^7.27.6" + "@babel/parser" "^7.28.0" + "@babel/template" "^7.27.2" + "@babel/traverse" "^7.28.0" + "@babel/types" "^7.28.0" + convert-source-map "^2.0.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.3" + semver "^6.3.1" + +"@babel/generator@^7.28.0": + version "7.28.0" + resolved "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz" + integrity sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg== + dependencies: + "@babel/parser" "^7.28.0" + "@babel/types" "^7.28.0" + "@jridgewell/gen-mapping" "^0.3.12" + "@jridgewell/trace-mapping" "^0.3.28" + jsesc "^3.0.2" + +"@babel/helper-compilation-targets@^7.27.2": + version "7.27.2" + resolved "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz" + integrity sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ== + dependencies: + "@babel/compat-data" "^7.27.2" + "@babel/helper-validator-option" "^7.27.1" + browserslist "^4.24.0" + lru-cache "^5.1.1" + semver "^6.3.1" + +"@babel/helper-globals@^7.28.0": + version "7.28.0" + resolved "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz" + integrity sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw== + +"@babel/helper-module-imports@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz" + integrity sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w== + dependencies: + "@babel/traverse" "^7.27.1" + "@babel/types" "^7.27.1" + +"@babel/helper-module-transforms@^7.27.3": + version "7.27.3" + resolved "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz" + integrity sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg== + dependencies: + "@babel/helper-module-imports" "^7.27.1" + "@babel/helper-validator-identifier" "^7.27.1" + "@babel/traverse" "^7.27.3" + +"@babel/helper-plugin-utils@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz" + integrity sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw== + +"@babel/helper-string-parser@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz" + integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA== + +"@babel/helper-validator-identifier@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz" + integrity sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow== + +"@babel/helper-validator-option@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz" + integrity sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg== + +"@babel/helpers@^7.27.6": + version "7.27.6" + resolved "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz" + integrity sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug== + dependencies: + "@babel/template" "^7.27.2" + "@babel/types" "^7.27.6" + +"@babel/parser@^7.1.0", "@babel/parser@^7.20.7", "@babel/parser@^7.27.2", "@babel/parser@^7.28.0": + version "7.28.0" + resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz" + integrity sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g== + dependencies: + "@babel/types" "^7.28.0" + +"@babel/plugin-transform-react-jsx-self@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz" + integrity sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-react-jsx-source@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz" + integrity sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/template@^7.27.2": + version "7.27.2" + resolved "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz" + integrity sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw== + dependencies: + "@babel/code-frame" "^7.27.1" + "@babel/parser" "^7.27.2" + "@babel/types" "^7.27.1" + +"@babel/traverse@^7.27.1", "@babel/traverse@^7.27.3", "@babel/traverse@^7.28.0": + version "7.28.0" + resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz" + integrity sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg== + dependencies: + "@babel/code-frame" "^7.27.1" + "@babel/generator" "^7.28.0" + "@babel/helper-globals" "^7.28.0" + "@babel/parser" "^7.28.0" + "@babel/template" "^7.27.2" + "@babel/types" "^7.28.0" + debug "^4.3.1" + +"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.27.1", "@babel/types@^7.27.6", "@babel/types@^7.28.0": + version "7.28.1" + resolved "https://registry.npmjs.org/@babel/types/-/types-7.28.1.tgz" + integrity sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ== + dependencies: + "@babel/helper-string-parser" "^7.27.1" + "@babel/helper-validator-identifier" "^7.27.1" + +"@esbuild/linux-x64@0.25.6": + version "0.25.6" + resolved "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.6.tgz" + integrity sha512-A6bJB41b4lKFWRKNrWoP2LHsjVzNiaurf7wyj/XtFNTsnPuxwEBWHLty+ZE0dWBKuSK1fvKgrKaNjBS7qbFKig== + +"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.7.0": + version "4.7.0" + resolved "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz" + integrity sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw== + dependencies: + eslint-visitor-keys "^3.4.3" + +"@eslint-community/regexpp@^4.10.0", "@eslint-community/regexpp@^4.12.1": + version "4.12.1" + resolved "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz" + integrity sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ== + +"@eslint/config-array@^0.21.0": + version "0.21.0" + resolved "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz" + integrity sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ== + dependencies: + "@eslint/object-schema" "^2.1.6" + debug "^4.3.1" + minimatch "^3.1.2" + +"@eslint/config-helpers@^0.3.0": + version "0.3.0" + resolved "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz" + integrity sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw== + +"@eslint/core@^0.15.0", "@eslint/core@^0.15.1": + version "0.15.1" + resolved "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz" + integrity sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA== + dependencies: + "@types/json-schema" "^7.0.15" + +"@eslint/eslintrc@^3.3.1": + version "3.3.1" + resolved "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz" + integrity sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ== + dependencies: + ajv "^6.12.4" + debug "^4.3.2" + espree "^10.0.1" + globals "^14.0.0" + ignore "^5.2.0" + import-fresh "^3.2.1" + js-yaml "^4.1.0" + minimatch "^3.1.2" + strip-json-comments "^3.1.1" + +"@eslint/js@^9.29.0", "@eslint/js@9.31.0": + version "9.31.0" + resolved "https://registry.npmjs.org/@eslint/js/-/js-9.31.0.tgz" + integrity sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw== + +"@eslint/object-schema@^2.1.6": + version "2.1.6" + resolved "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz" + integrity sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA== + +"@eslint/plugin-kit@^0.3.1": + version "0.3.3" + resolved "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.3.tgz" + integrity sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag== + dependencies: + "@eslint/core" "^0.15.1" + levn "^0.4.1" + +"@humanfs/core@^0.19.1": + version "0.19.1" + resolved "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz" + integrity sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA== + +"@humanfs/node@^0.16.6": + version "0.16.6" + resolved "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz" + integrity sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw== + dependencies: + "@humanfs/core" "^0.19.1" + "@humanwhocodes/retry" "^0.3.0" + +"@humanwhocodes/module-importer@^1.0.1": + version "1.0.1" + resolved "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz" + integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== + +"@humanwhocodes/retry@^0.3.0": + version "0.3.1" + resolved "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz" + integrity sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA== + +"@humanwhocodes/retry@^0.4.2": + version "0.4.3" + resolved "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz" + integrity sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ== + +"@jridgewell/gen-mapping@^0.3.12", "@jridgewell/gen-mapping@^0.3.5": + version "0.3.12" + resolved "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz" + integrity sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg== + dependencies: + "@jridgewell/sourcemap-codec" "^1.5.0" + "@jridgewell/trace-mapping" "^0.3.24" + +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.2" + resolved "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== + +"@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0": + version "1.5.4" + resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz" + integrity sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw== + +"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.28": + version "0.3.29" + resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz" + integrity sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== + dependencies: + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@^2.0.2", "@nodelib/fs.stat@2.0.5": + version "2.0.5" + resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + +"@nodelib/fs.walk@^1.2.3": + version "1.2.8" + resolved "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== + dependencies: + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" + +"@pkgr/core@^0.2.4": + version "0.2.7" + resolved "https://registry.npmjs.org/@pkgr/core/-/core-0.2.7.tgz" + integrity sha512-YLT9Zo3oNPJoBjBc4q8G2mjU4tqIbf5CEOORbUUr48dCD9q3umJ3IPlVqOqDakPfd2HuwccBaqlGhN4Gmr5OWg== + +"@rolldown/pluginutils@1.0.0-beta.27": + version "1.0.0-beta.27" + resolved "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz" + integrity sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA== + +"@rollup/rollup-linux-x64-gnu@4.45.1": + version "4.45.1" + resolved "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.45.1.tgz" + integrity sha512-+E/lYl6qu1zqgPEnTrs4WysQtvc/Sh4fC2nByfFExqgYrqkKWp1tWIbe+ELhixnenSpBbLXNi6vbEEJ8M7fiHw== + +"@types/babel__core@^7.20.5": + version "7.20.5" + resolved "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz" + integrity sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA== + dependencies: + "@babel/parser" "^7.20.7" + "@babel/types" "^7.20.7" + "@types/babel__generator" "*" + "@types/babel__template" "*" + "@types/babel__traverse" "*" + +"@types/babel__generator@*": + version "7.27.0" + resolved "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz" + integrity sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg== + dependencies: + "@babel/types" "^7.0.0" + +"@types/babel__template@*": + version "7.4.4" + resolved "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz" + integrity sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A== + dependencies: + "@babel/parser" "^7.1.0" + "@babel/types" "^7.0.0" + +"@types/babel__traverse@*": + version "7.20.7" + resolved "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz" + integrity sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng== + dependencies: + "@babel/types" "^7.20.7" + +"@types/estree@^1.0.6", "@types/estree@1.0.8": + version "1.0.8" + resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz" + integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w== + +"@types/json-schema@^7.0.15": + version "7.0.15" + resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== + +"@types/react-dom@^19.1.6": + version "19.1.6" + resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.6.tgz" + integrity sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw== + +"@types/react@^19.0.0", "@types/react@^19.1.8": + version "19.1.8" + resolved "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz" + integrity sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g== + dependencies: + csstype "^3.0.2" + +"@typescript-eslint/eslint-plugin@8.37.0": + version "8.37.0" + resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.37.0.tgz" + integrity sha512-jsuVWeIkb6ggzB+wPCsR4e6loj+rM72ohW6IBn2C+5NCvfUVY8s33iFPySSVXqtm5Hu29Ne/9bnA0JmyLmgenA== + dependencies: + "@eslint-community/regexpp" "^4.10.0" + "@typescript-eslint/scope-manager" "8.37.0" + "@typescript-eslint/type-utils" "8.37.0" + "@typescript-eslint/utils" "8.37.0" + "@typescript-eslint/visitor-keys" "8.37.0" + graphemer "^1.4.0" + ignore "^7.0.0" + natural-compare "^1.4.0" + ts-api-utils "^2.1.0" + +"@typescript-eslint/parser@^8.37.0", "@typescript-eslint/parser@8.37.0": + version "8.37.0" + resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.37.0.tgz" + integrity sha512-kVIaQE9vrN9RLCQMQ3iyRlVJpTiDUY6woHGb30JDkfJErqrQEmtdWH3gV0PBAfGZgQXoqzXOO0T3K6ioApbbAA== + dependencies: + "@typescript-eslint/scope-manager" "8.37.0" + "@typescript-eslint/types" "8.37.0" + "@typescript-eslint/typescript-estree" "8.37.0" + "@typescript-eslint/visitor-keys" "8.37.0" + debug "^4.3.4" + +"@typescript-eslint/project-service@8.37.0": + version "8.37.0" + resolved "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.37.0.tgz" + integrity sha512-BIUXYsbkl5A1aJDdYJCBAo8rCEbAvdquQ8AnLb6z5Lp1u3x5PNgSSx9A/zqYc++Xnr/0DVpls8iQ2cJs/izTXA== + dependencies: + "@typescript-eslint/tsconfig-utils" "^8.37.0" + "@typescript-eslint/types" "^8.37.0" + debug "^4.3.4" + +"@typescript-eslint/scope-manager@8.37.0": + version "8.37.0" + resolved "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.37.0.tgz" + integrity sha512-0vGq0yiU1gbjKob2q691ybTg9JX6ShiVXAAfm2jGf3q0hdP6/BruaFjL/ManAR/lj05AvYCH+5bbVo0VtzmjOA== + dependencies: + "@typescript-eslint/types" "8.37.0" + "@typescript-eslint/visitor-keys" "8.37.0" + +"@typescript-eslint/tsconfig-utils@^8.37.0", "@typescript-eslint/tsconfig-utils@8.37.0": + version "8.37.0" + resolved "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.37.0.tgz" + integrity sha512-1/YHvAVTimMM9mmlPvTec9NP4bobA1RkDbMydxG8omqwJJLEW/Iy2C4adsAESIXU3WGLXFHSZUU+C9EoFWl4Zg== + +"@typescript-eslint/type-utils@8.37.0": + version "8.37.0" + resolved "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.37.0.tgz" + integrity sha512-SPkXWIkVZxhgwSwVq9rqj/4VFo7MnWwVaRNznfQDc/xPYHjXnPfLWn+4L6FF1cAz6e7dsqBeMawgl7QjUMj4Ow== + dependencies: + "@typescript-eslint/types" "8.37.0" + "@typescript-eslint/typescript-estree" "8.37.0" + "@typescript-eslint/utils" "8.37.0" + debug "^4.3.4" + ts-api-utils "^2.1.0" + +"@typescript-eslint/types@^8.37.0", "@typescript-eslint/types@8.37.0": + version "8.37.0" + resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.37.0.tgz" + integrity sha512-ax0nv7PUF9NOVPs+lmQ7yIE7IQmAf8LGcXbMvHX5Gm+YJUYNAl340XkGnrimxZ0elXyoQJuN5sbg6C4evKA4SQ== + +"@typescript-eslint/typescript-estree@8.37.0": + version "8.37.0" + resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.37.0.tgz" + integrity sha512-zuWDMDuzMRbQOM+bHyU4/slw27bAUEcKSKKs3hcv2aNnc/tvE/h7w60dwVw8vnal2Pub6RT1T7BI8tFZ1fE+yg== + dependencies: + "@typescript-eslint/project-service" "8.37.0" + "@typescript-eslint/tsconfig-utils" "8.37.0" + "@typescript-eslint/types" "8.37.0" + "@typescript-eslint/visitor-keys" "8.37.0" + debug "^4.3.4" + fast-glob "^3.3.2" + is-glob "^4.0.3" + minimatch "^9.0.4" + semver "^7.6.0" + ts-api-utils "^2.1.0" + +"@typescript-eslint/utils@8.37.0": + version "8.37.0" + resolved "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.37.0.tgz" + integrity sha512-TSFvkIW6gGjN2p6zbXo20FzCABbyUAuq6tBvNRGsKdsSQ6a7rnV6ADfZ7f4iI3lIiXc4F4WWvtUfDw9CJ9pO5A== + dependencies: + "@eslint-community/eslint-utils" "^4.7.0" + "@typescript-eslint/scope-manager" "8.37.0" + "@typescript-eslint/types" "8.37.0" + "@typescript-eslint/typescript-estree" "8.37.0" + +"@typescript-eslint/visitor-keys@8.37.0": + version "8.37.0" + resolved "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.37.0.tgz" + integrity sha512-YzfhzcTnZVPiLfP/oeKtDp2evwvHLMe0LOy7oe+hb9KKIumLNohYS9Hgp1ifwpu42YWxhZE8yieggz6JpqO/1w== + dependencies: + "@typescript-eslint/types" "8.37.0" + eslint-visitor-keys "^4.2.1" + +"@vitejs/plugin-react@^4.5.2": + version "4.7.0" + resolved "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz" + integrity sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA== + dependencies: + "@babel/core" "^7.28.0" + "@babel/plugin-transform-react-jsx-self" "^7.27.1" + "@babel/plugin-transform-react-jsx-source" "^7.27.1" + "@rolldown/pluginutils" "1.0.0-beta.27" + "@types/babel__core" "^7.20.5" + react-refresh "^0.17.0" + +acorn-jsx@^5.3.2: + version "5.3.2" + resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== + +"acorn@^6.0.0 || ^7.0.0 || ^8.0.0", acorn@^8.15.0: + version "8.15.0" + resolved "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz" + integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg== + +ajv@^6.12.4: + version "6.12.6" + resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ansi-escapes@^7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz" + integrity sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw== + dependencies: + environment "^1.0.0" + +ansi-regex@^6.0.1: + version "6.1.0" + resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz" + integrity sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA== + +ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +ansi-styles@^6.0.0: + version "6.2.1" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz" + integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== + +ansi-styles@^6.2.1: + version "6.2.1" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz" + integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + +axios@^1.10.0: + version "1.10.0" + resolved "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz" + integrity sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw== + dependencies: + follow-redirects "^1.15.6" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +brace-expansion@^1.1.7: + version "1.1.12" + resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz" + integrity sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +brace-expansion@^2.0.1: + version "2.0.2" + resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz" + integrity sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ== + dependencies: + balanced-match "^1.0.0" + +braces@^3.0.3: + version "3.0.3" + resolved "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== + dependencies: + fill-range "^7.1.1" + +browserslist@^4.24.0, "browserslist@>= 4.21.0": + version "4.25.1" + resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz" + integrity sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw== + dependencies: + caniuse-lite "^1.0.30001726" + electron-to-chromium "^1.5.173" + node-releases "^2.0.19" + update-browserslist-db "^1.1.3" + +call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz" + integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +caniuse-lite@^1.0.30001726: + version "1.0.30001727" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz" + integrity sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q== + +chalk@^4.0.0: + version "4.1.2" + resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +chalk@^5.4.1: + version "5.4.1" + resolved "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz" + integrity sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w== + +cli-cursor@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz" + integrity sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw== + dependencies: + restore-cursor "^5.0.0" + +cli-truncate@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz" + integrity sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA== + dependencies: + slice-ansi "^5.0.0" + string-width "^7.0.0" + +clsx@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz" + integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA== + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +colorette@^2.0.20: + version "2.0.20" + resolved "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz" + integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== + +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +commander@^14.0.0: + version "14.0.0" + resolved "https://registry.npmjs.org/commander/-/commander-14.0.0.tgz" + integrity sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +convert-source-map@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz" + integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== + +cookie@^1.0.1: + version "1.0.2" + resolved "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz" + integrity sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA== + +cross-spawn@^7.0.6: + version "7.0.6" + resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz" + integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +csstype@^3.0.2: + version "3.1.3" + resolved "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz" + integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== + +debug@^4.1.0, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.4.1: + version "4.4.1" + resolved "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz" + integrity sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ== + dependencies: + ms "^2.1.3" + +deep-is@^0.1.3: + version "0.1.4" + resolved "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz" + integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + +dunder-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz" + integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== + dependencies: + call-bind-apply-helpers "^1.0.1" + es-errors "^1.3.0" + gopd "^1.2.0" + +electron-to-chromium@^1.5.173: + version "1.5.187" + resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.187.tgz" + integrity sha512-cl5Jc9I0KGUoOoSbxvTywTa40uspGJt/BDBoDLoxJRSBpWh4FFXBsjNRHfQrONsV/OoEjDfHUmZQa2d6Ze4YgA== + +emoji-regex@^10.3.0: + version "10.4.0" + resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz" + integrity sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw== + +environment@^1.0.0: + version "1.1.0" + resolved "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz" + integrity sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q== + +es-define-property@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz" + integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + +es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz" + integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA== + dependencies: + es-errors "^1.3.0" + +es-set-tostringtag@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz" + integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA== + dependencies: + es-errors "^1.3.0" + get-intrinsic "^1.2.6" + has-tostringtag "^1.0.2" + hasown "^2.0.2" + +esbuild@^0.25.0: + version "0.25.6" + resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.25.6.tgz" + integrity sha512-GVuzuUwtdsghE3ocJ9Bs8PNoF13HNQ5TXbEi2AhvVb8xU1Iwt9Fos9FEamfoee+u/TOsn7GUWc04lz46n2bbTg== + optionalDependencies: + "@esbuild/aix-ppc64" "0.25.6" + "@esbuild/android-arm" "0.25.6" + "@esbuild/android-arm64" "0.25.6" + "@esbuild/android-x64" "0.25.6" + "@esbuild/darwin-arm64" "0.25.6" + "@esbuild/darwin-x64" "0.25.6" + "@esbuild/freebsd-arm64" "0.25.6" + "@esbuild/freebsd-x64" "0.25.6" + "@esbuild/linux-arm" "0.25.6" + "@esbuild/linux-arm64" "0.25.6" + "@esbuild/linux-ia32" "0.25.6" + "@esbuild/linux-loong64" "0.25.6" + "@esbuild/linux-mips64el" "0.25.6" + "@esbuild/linux-ppc64" "0.25.6" + "@esbuild/linux-riscv64" "0.25.6" + "@esbuild/linux-s390x" "0.25.6" + "@esbuild/linux-x64" "0.25.6" + "@esbuild/netbsd-arm64" "0.25.6" + "@esbuild/netbsd-x64" "0.25.6" + "@esbuild/openbsd-arm64" "0.25.6" + "@esbuild/openbsd-x64" "0.25.6" + "@esbuild/openharmony-arm64" "0.25.6" + "@esbuild/sunos-x64" "0.25.6" + "@esbuild/win32-arm64" "0.25.6" + "@esbuild/win32-ia32" "0.25.6" + "@esbuild/win32-x64" "0.25.6" + +escalade@^3.2.0: + version "3.2.0" + resolved "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz" + integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== + +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +eslint-config-prettier@^10.1.5, "eslint-config-prettier@>= 7.0.0 <10.0.0 || >=10.1.0": + version "10.1.5" + resolved "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.5.tgz" + integrity sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw== + +eslint-plugin-prettier@^5.5.1: + version "5.5.1" + resolved "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.1.tgz" + integrity sha512-dobTkHT6XaEVOo8IO90Q4DOSxnm3Y151QxPJlM/vKC0bVy+d6cVWQZLlFiuZPP0wS6vZwSKeJgKkcS+KfMBlRw== + dependencies: + prettier-linter-helpers "^1.0.0" + synckit "^0.11.7" + +eslint-plugin-react-hooks@^5.2.0: + version "5.2.0" + resolved "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz" + integrity sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg== + +eslint-plugin-react-refresh@^0.4.20: + version "0.4.20" + resolved "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.20.tgz" + integrity sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA== + +eslint-scope@^8.4.0: + version "8.4.0" + resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz" + integrity sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg== + dependencies: + esrecurse "^4.3.0" + estraverse "^5.2.0" + +eslint-visitor-keys@^3.4.3: + version "3.4.3" + resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz" + integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== + +eslint-visitor-keys@^4.2.1: + version "4.2.1" + resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz" + integrity sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ== + +"eslint@^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0", "eslint@^6.0.0 || ^7.0.0 || >=8.0.0", "eslint@^8.57.0 || ^9.0.0", eslint@^9.31.0, eslint@>=7.0.0, eslint@>=8.0.0, eslint@>=8.40: + version "9.31.0" + resolved "https://registry.npmjs.org/eslint/-/eslint-9.31.0.tgz" + integrity sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ== + dependencies: + "@eslint-community/eslint-utils" "^4.2.0" + "@eslint-community/regexpp" "^4.12.1" + "@eslint/config-array" "^0.21.0" + "@eslint/config-helpers" "^0.3.0" + "@eslint/core" "^0.15.0" + "@eslint/eslintrc" "^3.3.1" + "@eslint/js" "9.31.0" + "@eslint/plugin-kit" "^0.3.1" + "@humanfs/node" "^0.16.6" + "@humanwhocodes/module-importer" "^1.0.1" + "@humanwhocodes/retry" "^0.4.2" + "@types/estree" "^1.0.6" + "@types/json-schema" "^7.0.15" + ajv "^6.12.4" + chalk "^4.0.0" + cross-spawn "^7.0.6" + debug "^4.3.2" + escape-string-regexp "^4.0.0" + eslint-scope "^8.4.0" + eslint-visitor-keys "^4.2.1" + espree "^10.4.0" + esquery "^1.5.0" + esutils "^2.0.2" + fast-deep-equal "^3.1.3" + file-entry-cache "^8.0.0" + find-up "^5.0.0" + glob-parent "^6.0.2" + ignore "^5.2.0" + imurmurhash "^0.1.4" + is-glob "^4.0.0" + json-stable-stringify-without-jsonify "^1.0.1" + lodash.merge "^4.6.2" + minimatch "^3.1.2" + natural-compare "^1.4.0" + optionator "^0.9.3" + +espree@^10.0.1, espree@^10.4.0: + version "10.4.0" + resolved "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz" + integrity sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ== + dependencies: + acorn "^8.15.0" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^4.2.1" + +esquery@^1.5.0: + version "1.6.0" + resolved "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz" + integrity sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg== + dependencies: + estraverse "^5.1.0" + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^5.1.0, estraverse@^5.2.0: + version "5.3.0" + resolved "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +eventemitter3@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz" + integrity sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA== + +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-diff@^1.1.2: + version "1.3.0" + resolved "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz" + integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw== + +fast-glob@^3.3.2: + version "3.3.3" + resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz" + integrity sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.8" + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-levenshtein@^2.0.6: + version "2.0.6" + resolved "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz" + integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== + +fastq@^1.6.0: + version "1.19.1" + resolved "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz" + integrity sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ== + dependencies: + reusify "^1.0.4" + +fdir@^6.4.4: + version "6.4.6" + resolved "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz" + integrity sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w== + +fdir@^6.4.6: + version "6.4.6" + resolved "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz" + integrity sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w== + +file-entry-cache@^8.0.0: + version "8.0.0" + resolved "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz" + integrity sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ== + dependencies: + flat-cache "^4.0.0" + +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + dependencies: + to-regex-range "^5.0.1" + +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +flat-cache@^4.0.0: + version "4.0.1" + resolved "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz" + integrity sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw== + dependencies: + flatted "^3.2.9" + keyv "^4.5.4" + +flatted@^3.2.9: + version "3.3.3" + resolved "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz" + integrity sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg== + +follow-redirects@^1.15.6: + version "1.15.9" + resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz" + integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ== + +form-data@^4.0.0: + version "4.0.4" + resolved "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz" + integrity sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + es-set-tostringtag "^2.1.0" + hasown "^2.0.2" + mime-types "^2.1.12" + +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +gensync@^1.0.0-beta.2: + version "1.0.0-beta.2" + resolved "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz" + integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== + +get-east-asian-width@^1.0.0: + version "1.3.0" + resolved "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz" + integrity sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ== + +get-intrinsic@^1.2.6: + version "1.3.0" + resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz" + integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== + dependencies: + call-bind-apply-helpers "^1.0.2" + es-define-property "^1.0.1" + es-errors "^1.3.0" + es-object-atoms "^1.1.1" + function-bind "^1.1.2" + get-proto "^1.0.1" + gopd "^1.2.0" + has-symbols "^1.1.0" + hasown "^2.0.2" + math-intrinsics "^1.1.0" + +get-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz" + integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== + dependencies: + dunder-proto "^1.0.1" + es-object-atoms "^1.0.0" + +glob-parent@^5.1.2: + version "5.1.2" + resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob-parent@^6.0.2: + version "6.0.2" + resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + +globals@^14.0.0: + version "14.0.0" + resolved "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz" + integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ== + +globals@^16.2.0: + version "16.3.0" + resolved "https://registry.npmjs.org/globals/-/globals-16.3.0.tgz" + integrity sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ== + +gopd@^1.2.0: + version "1.2.0" + resolved "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz" + integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== + +graphemer@^1.4.0: + version "1.4.0" + resolved "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz" + integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-symbols@^1.0.3, has-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz" + integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== + +has-tostringtag@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz" + integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== + dependencies: + has-symbols "^1.0.3" + +hasown@^2.0.2: + version "2.0.2" + resolved "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + +husky@^8.0.0: + version "8.0.3" + resolved "https://registry.npmjs.org/husky/-/husky-8.0.3.tgz" + integrity sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg== + +ignore@^5.2.0: + version "5.3.2" + resolved "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz" + integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== + +ignore@^7.0.0: + version "7.0.5" + resolved "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz" + integrity sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg== + +import-fresh@^3.2.1: + version "3.3.1" + resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz" + integrity sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz" + integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-fullwidth-code-point@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz" + integrity sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ== + +is-fullwidth-code-point@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz" + integrity sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA== + dependencies: + get-east-asian-width "^1.0.0" + +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3: + version "4.0.3" + resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + +jsesc@^3.0.2: + version "3.1.0" + resolved "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz" + integrity sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA== + +json-buffer@3.0.1: + version "3.0.1" + resolved "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz" + integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz" + integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== + +json5@^2.2.3: + version "2.2.3" + resolved "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz" + integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== + +keyv@^4.5.4: + version "4.5.4" + resolved "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz" + integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== + dependencies: + json-buffer "3.0.1" + +levn@^0.4.1: + version "0.4.1" + resolved "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz" + integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== + dependencies: + prelude-ls "^1.2.1" + type-check "~0.4.0" + +lilconfig@^3.1.3: + version "3.1.3" + resolved "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz" + integrity sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw== + +lint-staged@^16.1.2: + version "16.1.2" + resolved "https://registry.npmjs.org/lint-staged/-/lint-staged-16.1.2.tgz" + integrity sha512-sQKw2Si2g9KUZNY3XNvRuDq4UJqpHwF0/FQzZR2M7I5MvtpWvibikCjUVJzZdGE0ByurEl3KQNvsGetd1ty1/Q== + dependencies: + chalk "^5.4.1" + commander "^14.0.0" + debug "^4.4.1" + lilconfig "^3.1.3" + listr2 "^8.3.3" + micromatch "^4.0.8" + nano-spawn "^1.0.2" + pidtree "^0.6.0" + string-argv "^0.3.2" + yaml "^2.8.0" + +listr2@^8.3.3: + version "8.3.3" + resolved "https://registry.npmjs.org/listr2/-/listr2-8.3.3.tgz" + integrity sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ== + dependencies: + cli-truncate "^4.0.0" + colorette "^2.0.20" + eventemitter3 "^5.0.1" + log-update "^6.1.0" + rfdc "^1.4.1" + wrap-ansi "^9.0.0" + +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + +lodash.merge@^4.6.2: + version "4.6.2" + resolved "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz" + integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== + +log-update@^6.1.0: + version "6.1.0" + resolved "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz" + integrity sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w== + dependencies: + ansi-escapes "^7.0.0" + cli-cursor "^5.0.0" + slice-ansi "^7.1.0" + strip-ansi "^7.1.0" + wrap-ansi "^9.0.0" + +lru-cache@^5.1.1: + version "5.1.1" + resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== + dependencies: + yallist "^3.0.2" + +math-intrinsics@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz" + integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== + +merge2@^1.3.0: + version "1.4.1" + resolved "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + +micromatch@^4.0.8: + version "4.0.8" + resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz" + integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== + dependencies: + braces "^3.0.3" + picomatch "^2.3.1" + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.12: + version "2.1.35" + resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mimic-function@^5.0.0: + version "5.0.1" + resolved "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz" + integrity sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA== + +minimatch@^3.1.2: + version "3.1.2" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimatch@^9.0.4: + version "9.0.5" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz" + integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== + dependencies: + brace-expansion "^2.0.1" + +ms@^2.1.3: + version "2.1.3" + resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +nano-spawn@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/nano-spawn/-/nano-spawn-1.0.2.tgz" + integrity sha512-21t+ozMQDAL/UGgQVBbZ/xXvNO10++ZPuTmKRO8k9V3AClVRht49ahtDjfY8l1q6nSHOrE5ASfthzH3ol6R/hg== + +nanoid@^3.3.11: + version "3.3.11" + resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz" + integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w== + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz" + integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== + +node-releases@^2.0.19: + version "2.0.19" + resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz" + integrity sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw== + +onetime@^7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz" + integrity sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ== + dependencies: + mimic-function "^5.0.0" + +optionator@^0.9.3: + version "0.9.4" + resolved "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz" + integrity sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g== + dependencies: + deep-is "^0.1.3" + fast-levenshtein "^2.0.6" + levn "^0.4.1" + prelude-ls "^1.2.1" + type-check "^0.4.0" + word-wrap "^1.2.5" + +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +picocolors@^1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== + +picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +"picomatch@^3 || ^4", picomatch@^4.0.2: + version "4.0.3" + resolved "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz" + integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q== + +pidtree@^0.6.0: + version "0.6.0" + resolved "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz" + integrity sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g== + +postcss@^8.5.6: + version "8.5.6" + resolved "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz" + integrity sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg== + dependencies: + nanoid "^3.3.11" + picocolors "^1.1.1" + source-map-js "^1.2.1" + +prelude-ls@^1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz" + integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== + +prettier-linter-helpers@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz" + integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w== + dependencies: + fast-diff "^1.1.2" + +prettier@^3.6.2, prettier@>=3.0.0: + version "3.6.2" + resolved "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz" + integrity sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ== + +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + +punycode@^2.1.0: + version "2.3.1" + resolved "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== + +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + +"react-dom@^18 || ^19", react-dom@^19.1.0, react-dom@>=18: + version "19.1.0" + resolved "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz" + integrity sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g== + dependencies: + scheduler "^0.26.0" + +react-refresh@^0.17.0: + version "0.17.0" + resolved "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz" + integrity sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ== + +react-router-dom@^7.7.0: + version "7.7.0" + resolved "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.7.0.tgz" + integrity sha512-wwGS19VkNBkneVh9/YD0pK3IsjWxQUVMDD6drlG7eJpo1rXBtctBqDyBm/k+oKHRAm1x9XWT3JFC82QI9YOXXA== + dependencies: + react-router "7.7.0" + +react-router@7.7.0: + version "7.7.0" + resolved "https://registry.npmjs.org/react-router/-/react-router-7.7.0.tgz" + integrity sha512-3FUYSwlvB/5wRJVTL/aavqHmfUKe0+Xm9MllkYgGo9eDwNdkvwlJGjpPxono1kCycLt6AnDTgjmXvK3/B4QGuw== + dependencies: + cookie "^1.0.1" + set-cookie-parser "^2.6.0" + +react-toastify@^11.0.5: + version "11.0.5" + resolved "https://registry.npmjs.org/react-toastify/-/react-toastify-11.0.5.tgz" + integrity sha512-EpqHBGvnSTtHYhCPLxML05NLY2ZX0JURbAdNYa6BUkk+amz4wbKBQvoKQAB0ardvSarUBuY4Q4s1sluAzZwkmA== + dependencies: + clsx "^2.1.1" + +"react@^18 || ^19", react@^19.1.0, react@>=18: + version "19.1.0" + resolved "https://registry.npmjs.org/react/-/react-19.1.0.tgz" + integrity sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg== + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +restore-cursor@^5.0.0: + version "5.1.0" + resolved "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz" + integrity sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA== + dependencies: + onetime "^7.0.0" + signal-exit "^4.1.0" + +reusify@^1.0.4: + version "1.1.0" + resolved "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz" + integrity sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw== + +rfdc@^1.4.1: + version "1.4.1" + resolved "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz" + integrity sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA== + +rollup@^4.40.0: + version "4.45.1" + resolved "https://registry.npmjs.org/rollup/-/rollup-4.45.1.tgz" + integrity sha512-4iya7Jb76fVpQyLoiVpzUrsjQ12r3dM7fIVz+4NwoYvZOShknRmiv+iu9CClZml5ZLGb0XMcYLutK6w9tgxHDw== + dependencies: + "@types/estree" "1.0.8" + optionalDependencies: + "@rollup/rollup-android-arm-eabi" "4.45.1" + "@rollup/rollup-android-arm64" "4.45.1" + "@rollup/rollup-darwin-arm64" "4.45.1" + "@rollup/rollup-darwin-x64" "4.45.1" + "@rollup/rollup-freebsd-arm64" "4.45.1" + "@rollup/rollup-freebsd-x64" "4.45.1" + "@rollup/rollup-linux-arm-gnueabihf" "4.45.1" + "@rollup/rollup-linux-arm-musleabihf" "4.45.1" + "@rollup/rollup-linux-arm64-gnu" "4.45.1" + "@rollup/rollup-linux-arm64-musl" "4.45.1" + "@rollup/rollup-linux-loongarch64-gnu" "4.45.1" + "@rollup/rollup-linux-powerpc64le-gnu" "4.45.1" + "@rollup/rollup-linux-riscv64-gnu" "4.45.1" + "@rollup/rollup-linux-riscv64-musl" "4.45.1" + "@rollup/rollup-linux-s390x-gnu" "4.45.1" + "@rollup/rollup-linux-x64-gnu" "4.45.1" + "@rollup/rollup-linux-x64-musl" "4.45.1" + "@rollup/rollup-win32-arm64-msvc" "4.45.1" + "@rollup/rollup-win32-ia32-msvc" "4.45.1" + "@rollup/rollup-win32-x64-msvc" "4.45.1" + fsevents "~2.3.2" + +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + +scheduler@^0.26.0: + version "0.26.0" + resolved "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz" + integrity sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA== + +semver@^6.3.1: + version "6.3.1" + resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + +semver@^7.6.0: + version "7.7.2" + resolved "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz" + integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA== + +set-cookie-parser@^2.6.0: + version "2.7.1" + resolved "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz" + integrity sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ== + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +signal-exit@^4.1.0: + version "4.1.0" + resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz" + integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== + +slice-ansi@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz" + integrity sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ== + dependencies: + ansi-styles "^6.0.0" + is-fullwidth-code-point "^4.0.0" + +slice-ansi@^7.1.0: + version "7.1.0" + resolved "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz" + integrity sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg== + dependencies: + ansi-styles "^6.2.1" + is-fullwidth-code-point "^5.0.0" + +source-map-js@^1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz" + integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== + +string-argv@^0.3.2: + version "0.3.2" + resolved "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz" + integrity sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q== + +string-width@^7.0.0: + version "7.2.0" + resolved "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz" + integrity sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ== + dependencies: + emoji-regex "^10.3.0" + get-east-asian-width "^1.0.0" + strip-ansi "^7.1.0" + +strip-ansi@^7.1.0: + version "7.1.0" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz" + integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== + dependencies: + ansi-regex "^6.0.1" + +strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +synckit@^0.11.7: + version "0.11.8" + resolved "https://registry.npmjs.org/synckit/-/synckit-0.11.8.tgz" + integrity sha512-+XZ+r1XGIJGeQk3VvXhT6xx/VpbHsRzsTkGgF6E5RX9TTXD0118l87puaEBZ566FhqblC6U0d4XnubznJDm30A== + dependencies: + "@pkgr/core" "^0.2.4" + +tinyglobby@^0.2.14: + version "0.2.14" + resolved "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz" + integrity sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ== + dependencies: + fdir "^6.4.4" + picomatch "^4.0.2" + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +ts-api-utils@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz" + integrity sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ== + +type-check@^0.4.0, type-check@~0.4.0: + version "0.4.0" + resolved "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz" + integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== + dependencies: + prelude-ls "^1.2.1" + +typescript-eslint@^8.34.1: + version "8.37.0" + resolved "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.37.0.tgz" + integrity sha512-TnbEjzkE9EmcO0Q2zM+GE8NQLItNAJpMmED1BdgoBMYNdqMhzlbqfdSwiRlAzEK2pA9UzVW0gzaaIzXWg2BjfA== + dependencies: + "@typescript-eslint/eslint-plugin" "8.37.0" + "@typescript-eslint/parser" "8.37.0" + "@typescript-eslint/typescript-estree" "8.37.0" + "@typescript-eslint/utils" "8.37.0" + +typescript@>=4.8.4, "typescript@>=4.8.4 <5.9.0", typescript@~5.8.3: + version "5.8.3" + resolved "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz" + integrity sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ== + +update-browserslist-db@^1.1.3: + version "1.1.3" + resolved "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz" + integrity sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw== + dependencies: + escalade "^3.2.0" + picocolors "^1.1.1" + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +"vite@^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0", vite@^7.0.0: + version "7.0.5" + resolved "https://registry.npmjs.org/vite/-/vite-7.0.5.tgz" + integrity sha512-1mncVwJxy2C9ThLwz0+2GKZyEXuC3MyWtAAlNftlZZXZDP3AJt5FmwcMit/IGGaNZ8ZOB2BNO/HFUB+CpN0NQw== + dependencies: + esbuild "^0.25.0" + fdir "^6.4.6" + picomatch "^4.0.2" + postcss "^8.5.6" + rollup "^4.40.0" + tinyglobby "^0.2.14" + optionalDependencies: + fsevents "~2.3.3" + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +word-wrap@^1.2.5: + version "1.2.5" + resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz" + integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== + +wrap-ansi@^9.0.0: + version "9.0.0" + resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz" + integrity sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q== + dependencies: + ansi-styles "^6.2.1" + string-width "^7.0.0" + strip-ansi "^7.1.0" + +yallist@^3.0.2: + version "3.1.1" + resolved "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== + +yaml@^2.4.2, yaml@^2.8.0: + version "2.8.0" + resolved "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz" + integrity sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ== + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== From af7649ee6d7edfd2e3153365065c9b38484af2b0 Mon Sep 17 00:00:00 2001 From: VishakhaSainani-Josh Date: Mon, 21 Jul 2025 15:24:28 +0530 Subject: [PATCH 03/36] use same gitignore for both rfontend and backend --- .gitignore | 27 ++++++++++++++++++++++++++- frontend/.gitignore | 24 ------------------------ frontend/yarn.lock | 5 +++++ 3 files changed, 31 insertions(+), 25 deletions(-) delete mode 100644 frontend/.gitignore diff --git a/.gitignore b/.gitignore index 9890de43..077bbe57 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,26 @@ -local.yaml \ No newline at end of file +local.yaml + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend/.gitignore b/frontend/.gitignore deleted file mode 100644 index a547bf36..00000000 --- a/frontend/.gitignore +++ /dev/null @@ -1,24 +0,0 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* - -node_modules -dist -dist-ssr -*.local - -# Editor directories and files -.vscode/* -!.vscode/extensions.json -.idea -.DS_Store -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? diff --git a/frontend/yarn.lock b/frontend/yarn.lock index eacee312..57c86129 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -329,6 +329,11 @@ resolved "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.45.1.tgz" integrity sha512-+E/lYl6qu1zqgPEnTrs4WysQtvc/Sh4fC2nByfFExqgYrqkKWp1tWIbe+ELhixnenSpBbLXNi6vbEEJ8M7fiHw== +"@rollup/rollup-linux-x64-musl@4.45.1": + version "4.45.1" + resolved "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.45.1.tgz" + integrity sha512-a6WIAp89p3kpNoYStITT9RbTbTnqarU7D8N8F2CV+4Cl9fwCOZraLVuVFvlpsW0SbIiYtEnhCZBPLoNdRkjQFw== + "@types/babel__core@^7.20.5": version "7.20.5" resolved "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz" From d0f42abaaa7e82f4844e398153879bcb8249ed3e Mon Sep 17 00:00:00 2001 From: VishakhaSainani-Josh Date: Mon, 21 Jul 2025 15:54:34 +0530 Subject: [PATCH 04/36] change title, use handler for using context value and add interceptor in axios --- frontend/index.html | 2 +- frontend/src/context/AuthProvider.tsx | 46 +++++++++++++++++---------- frontend/src/utils/axios.ts | 12 +++++++ 3 files changed, 42 insertions(+), 18 deletions(-) diff --git a/frontend/index.html b/frontend/index.html index e4b78eae..7c5230bb 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,7 +4,7 @@ - Vite + React + TS + Code Curiosity
diff --git a/frontend/src/context/AuthProvider.tsx b/frontend/src/context/AuthProvider.tsx index a2165f30..5301cee5 100644 --- a/frontend/src/context/AuthProvider.tsx +++ b/frontend/src/context/AuthProvider.tsx @@ -2,9 +2,7 @@ import { createContext, useMemo, useState, - type Dispatch, type ReactNode, - type SetStateAction, } from "react"; export type User = { @@ -15,17 +13,25 @@ export type User = { export interface UserContextInterface { user: User; - setUser: Dispatch>; + login: (user: User, token: string) => void; + logout: () => void; } -const defaultState = { - user: { - githubId: "", - githubUsername: "", - avatarUrl: "", +const defaultUser: User = { + githubId: "", + githubUsername: "", + avatarUrl: "", +}; + +const defaultState: UserContextInterface = { + user: defaultUser, + login: () => { + throw new Error("login must be used within UserProvider"); + }, + logout: () => { + throw new Error("logout must be used within UserProvider"); }, - setUser: (user: User) => {}, -} as UserContextInterface; +}; export const UserContext = createContext(defaultState); @@ -34,13 +40,19 @@ type UserProviderProps = { }; export const UserProvider = ({ children }: UserProviderProps) => { - const [user, setUser] = useState(defaultState.user); + const [user, setUser] = useState(defaultUser); + + const login = (newUser: User, token: string) => { + setUser(newUser); + localStorage.setItem("token", token); + }; + + const logout = () => { + setUser(defaultUser); + localStorage.removeItem("token"); + }; - const memoizedUserValue = useMemo(() => ({ user, setUser }), [user, setUser]); + const value = useMemo(() => ({ user, login, logout }), [user]); - return ( - - {children} - - ); + return {children}; }; diff --git a/frontend/src/utils/axios.ts b/frontend/src/utils/axios.ts index fdfb3f57..d5f800d9 100644 --- a/frontend/src/utils/axios.ts +++ b/frontend/src/utils/axios.ts @@ -3,3 +3,15 @@ import axios from "axios"; export const api = axios.create({ baseURL: "http://localhost:8080/api/v1", }); + +api.interceptors.request.use( + (config) => { + const token = localStorage.getItem("token"); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; + }, + (error) => Promise.reject(error) + +); From 1d13c04edd249c81d3ee061626f9033b6e051bdb Mon Sep 17 00:00:00 2001 From: VishakhaSainani-Josh Date: Wed, 23 Jul 2025 12:23:54 +0530 Subject: [PATCH 05/36] implemented login page --- .gitignore | 1 + backend/run-backend.sh | 4 + frontend/components.json | 21 + frontend/package-lock.json | 877 ++++++++++++++++-- frontend/package.json | 16 +- frontend/src/App.css | 42 - frontend/src/App.tsx | 5 +- frontend/src/assets/coder.svg | 9 + frontend/src/components/ui/button.tsx | 59 ++ frontend/src/components/ui/card.tsx | 92 ++ frontend/src/context/AuthProvider.tsx | 23 +- frontend/src/index.css | 214 ++++- frontend/src/lib/utils.ts | 6 + frontend/src/main.tsx | 18 +- frontend/src/pages/Login/index.tsx | 13 + frontend/src/pages/Login/loginComponent.tsx | 45 + frontend/src/pages/Login/main.tsx | 9 - frontend/src/root/PrivateRoutes.tsx | 12 +- frontend/src/root/Router.tsx | 6 +- frontend/src/root/routeConstants.ts | 4 +- frontend/src/root/routesConfig.tsx | 10 +- frontend/src/shared/components/AuthLayout.tsx | 87 ++ frontend/src/shared/lib/constants.ts | 2 + frontend/src/shared/types/auth.ts | 16 + frontend/src/shared/utils/local-storage.ts | 29 + frontend/src/utils/axios.ts | 11 +- frontend/src/utils/endpoints.ts | 5 + frontend/src/utils/react-query.ts | 11 + frontend/src/vite-env.d.ts | 9 + frontend/tailwind.config.js | 63 ++ frontend/tsconfig.app.json | 6 + frontend/tsconfig.json | 2 +- frontend/vite.config.ts | 11 +- frontend/yarn.lock | 252 ++++- package-lock.json | 6 + 35 files changed, 1770 insertions(+), 226 deletions(-) create mode 100644 backend/run-backend.sh create mode 100644 frontend/components.json delete mode 100644 frontend/src/App.css create mode 100644 frontend/src/assets/coder.svg create mode 100644 frontend/src/components/ui/button.tsx create mode 100644 frontend/src/components/ui/card.tsx create mode 100644 frontend/src/lib/utils.ts create mode 100644 frontend/src/pages/Login/index.tsx create mode 100644 frontend/src/pages/Login/loginComponent.tsx delete mode 100644 frontend/src/pages/Login/main.tsx create mode 100644 frontend/src/shared/components/AuthLayout.tsx create mode 100644 frontend/src/shared/lib/constants.ts create mode 100644 frontend/src/shared/types/auth.ts create mode 100644 frontend/src/shared/utils/local-storage.ts create mode 100644 frontend/src/utils/endpoints.ts create mode 100644 frontend/src/utils/react-query.ts create mode 100644 frontend/tailwind.config.js create mode 100644 package-lock.json diff --git a/.gitignore b/.gitignore index 077bbe57..0197edab 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ local.yaml +.env # Logs logs diff --git a/backend/run-backend.sh b/backend/run-backend.sh new file mode 100644 index 00000000..3ca49127 --- /dev/null +++ b/backend/run-backend.sh @@ -0,0 +1,4 @@ +export CONFIG_PATH=local.yaml + +export GOOGLE_APPLICATION_CREDENTIALS=/home/josh/Documents/Codez/cobalt-alliance-459708-h5-3e1df629b2ff.json +go run ./cmd/main.go \ No newline at end of file diff --git a/frontend/components.json b/frontend/components.json new file mode 100644 index 00000000..1d282e64 --- /dev/null +++ b/frontend/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.js", + "css": "src/index.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 2f392ce8..f66cef2c 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,17 +8,28 @@ "name": "code-curiosity-frontend", "version": "0.0.0", "dependencies": { + "@radix-ui/react-slot": "^1.2.3", + "@tailwindcss/vite": "^4.1.11", + "@tanstack/react-query": "^5.83.0", + "@tanstack/react-query-devtools": "^5.83.0", "axios": "^1.10.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "dotenv": "^17.2.0", + "lucide-react": "^0.525.0", "react": "^19.1.0", "react-dom": "^19.1.0", "react-router-dom": "^7.7.0", - "react-toastify": "^11.0.5" + "react-toastify": "^11.0.5", + "tailwind-merge": "^3.3.1", + "tailwindcss": "^4.1.11" }, "devDependencies": { "@eslint/js": "^9.29.0", + "@types/node": "^24.0.15", "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", - "@vitejs/plugin-react": "^4.5.2", + "@vitejs/plugin-react": "^4.7.0", "eslint": "^9.31.0", "eslint-config-prettier": "^10.1.5", "eslint-plugin-prettier": "^5.5.1", @@ -28,6 +39,7 @@ "husky": "^8.0.0", "lint-staged": "^16.1.2", "prettier": "^3.6.2", + "tw-animate-css": "^1.3.5", "typescript": "~5.8.3", "typescript-eslint": "^8.34.1", "vite": "^7.0.0" @@ -37,7 +49,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -336,7 +347,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -353,7 +363,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -370,7 +379,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -387,7 +395,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -404,7 +411,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -421,7 +427,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -438,7 +443,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -455,7 +459,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -472,7 +475,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -489,7 +491,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -506,7 +507,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -523,7 +523,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -540,7 +539,6 @@ "cpu": [ "mips64el" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -557,7 +555,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -574,7 +571,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -591,7 +587,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -608,7 +603,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -625,7 +619,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -642,7 +635,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -659,7 +651,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -676,7 +667,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -693,7 +683,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -710,7 +699,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -727,7 +715,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -744,7 +731,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -761,7 +747,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -991,11 +976,22 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.12", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -1006,7 +1002,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -1016,14 +1011,12 @@ "version": "1.5.4", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", - "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.29", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -1081,6 +1074,39 @@ "url": "https://opencollective.com/pkgr" } }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.27", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", @@ -1095,7 +1121,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1109,7 +1134,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1123,7 +1147,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1137,7 +1160,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1151,7 +1173,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1165,7 +1186,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1179,7 +1199,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1193,7 +1212,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1207,7 +1225,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1221,7 +1238,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1235,7 +1251,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1249,7 +1264,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1263,7 +1277,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1277,7 +1290,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1291,7 +1303,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1305,7 +1316,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1319,7 +1329,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1333,7 +1342,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1347,7 +1355,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1361,13 +1368,327 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "win32" ] }, + "node_modules/@tailwindcss/node": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.11.tgz", + "integrity": "sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q==", + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "enhanced-resolve": "^5.18.1", + "jiti": "^2.4.2", + "lightningcss": "1.30.1", + "magic-string": "^0.30.17", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.11" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.11.tgz", + "integrity": "sha512-Q69XzrtAhuyfHo+5/HMgr1lAiPP/G40OMFAnws7xcFEYqcypZmdW8eGXaOUIeOl1dzPJBPENXgbjsOyhg2nkrg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.4", + "tar": "^7.4.3" + }, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@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" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.11.tgz", + "integrity": "sha512-3IfFuATVRUMZZprEIx9OGDjG3Ou3jG4xQzNTvjDoKmU9JdmoCohQJ83MYd0GPnQIu89YoJqvMM0G3uqLRFtetg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.11.tgz", + "integrity": "sha512-ESgStEOEsyg8J5YcMb1xl8WFOXfeBmrhAwGsFxxB2CxY9evy63+AtpbDLAyRkJnxLy2WsD1qF13E97uQyP1lfQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.11.tgz", + "integrity": "sha512-EgnK8kRchgmgzG6jE10UQNaH9Mwi2n+yw1jWmof9Vyg2lpKNX2ioe7CJdf9M5f8V9uaQxInenZkOxnTVL3fhAw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.11.tgz", + "integrity": "sha512-xdqKtbpHs7pQhIKmqVpxStnY1skuNh4CtbcyOHeX1YBE0hArj2romsFGb6yUmzkq/6M24nkxDqU8GYrKrz+UcA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.11.tgz", + "integrity": "sha512-ryHQK2eyDYYMwB5wZL46uoxz2zzDZsFBwfjssgB7pzytAeCCa6glsiJGjhTEddq/4OsIjsLNMAiMlHNYnkEEeg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.11.tgz", + "integrity": "sha512-mYwqheq4BXF83j/w75ewkPJmPZIqqP1nhoghS9D57CLjsh3Nfq0m4ftTotRYtGnZd3eCztgbSPJ9QhfC91gDZQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.11.tgz", + "integrity": "sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.11.tgz", + "integrity": "sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.11.tgz", + "integrity": "sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.11.tgz", + "integrity": "sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@emnapi/wasi-threads": "^1.0.2", + "@napi-rs/wasm-runtime": "^0.2.11", + "@tybys/wasm-util": "^0.9.0", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.11.tgz", + "integrity": "sha512-UgKYx5PwEKrac3GPNPf6HVMNhUIGuUh4wlDFR2jYYdkX6pL/rn73zTq/4pzUm8fOjAn5L8zDeHp9iXmUGOXZ+w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.11.tgz", + "integrity": "sha512-YfHoggn1j0LK7wR82TOucWc5LDCguHnoS879idHekmmiR7g9HUtMw9MI0NHatS28u/Xlkfi9w5RJWgz2Dl+5Qg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.11.tgz", + "integrity": "sha512-RHYhrR3hku0MJFRV+fN2gNbDNEh3dwKvY8XJvTxCSXeMOsCRSr+uKvDWQcbizrHgjML6ZmTE5OwMrl5wKcujCw==", + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.1.11", + "@tailwindcss/oxide": "4.1.11", + "tailwindcss": "4.1.11" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7" + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.83.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.83.0.tgz", + "integrity": "sha512-0M8dA+amXUkyz5cVUm/B+zSk3xkQAcuXuz5/Q/LveT4ots2rBpPTZOzd7yJa2Utsf8D2Upl5KyjhHRY+9lB/XA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/query-devtools": { + "version": "5.81.2", + "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.81.2.tgz", + "integrity": "sha512-jCeJcDCwKfoyyBXjXe9+Lo8aTkavygHHsUHAlxQKKaDeyT0qyQNLKl7+UyqYH2dDF6UN/14873IPBHchcsU+Zg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.83.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.83.0.tgz", + "integrity": "sha512-/XGYhZ3foc5H0VM2jLSD/NyBRIOK4q9kfeml4+0x2DlL6xVuAcVEW+hTlTapAmejObg0i3eNqhkr2dT+eciwoQ==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.83.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@tanstack/react-query-devtools": { + "version": "5.83.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.83.0.tgz", + "integrity": "sha512-yfp8Uqd3I1jgx8gl0lxbSSESu5y4MO2ThOPBnGNTYs0P+ZFu+E9g5IdOngyUGuo6Uz6Qa7p9TLdZEX3ntik2fQ==", + "license": "MIT", + "dependencies": { + "@tanstack/query-devtools": "5.81.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-query": "^5.83.0", + "react": "^18 || ^19" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1417,7 +1738,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, "license": "MIT" }, "node_modules/@types/json-schema": { @@ -1427,11 +1747,21 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/node": { + "version": "24.0.15", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.15.tgz", + "integrity": "sha512-oaeTSbCef7U/z7rDeJA138xpG3NuKc64/rZ2qmUFkFJmnMsAPaluIifqyWd8hSSMxyP9oie3dLAqYPblag9KgA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.8.0" + } + }, "node_modules/@types/react": { "version": "19.1.8", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz", "integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.0.2" @@ -1973,6 +2303,27 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, "node_modules/cli-cursor": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", @@ -2106,7 +2457,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/debug": { @@ -2143,6 +2494,27 @@ "node": ">=0.4.0" } }, + "node_modules/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "17.2.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.0.tgz", + "integrity": "sha512-Q4sgBT60gzd0BB0lSyYD3xM4YxrXA9y4uBDof1JNYGzOXrQdQ6yX+7XIAqoFOGQFOTK1D3Hts5OllpxMDZFONQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com/" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -2171,6 +2543,19 @@ "dev": true, "license": "MIT" }, + "node_modules/enhanced-resolve": { + "version": "5.18.2", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz", + "integrity": "sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/environment": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", @@ -2233,7 +2618,6 @@ "version": "0.25.6", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.6.tgz", "integrity": "sha512-GVuzuUwtdsghE3ocJ9Bs8PNoF13HNQ5TXbEi2AhvVb8xU1Iwt9Fos9FEamfoee+u/TOsn7GUWc04lz46n2bbTg==", - "dev": true, "hasInstallScript": true, "license": "MIT", "bin": { @@ -2698,7 +3082,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -2816,6 +3199,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -2978,6 +3367,15 @@ "dev": true, "license": "ISC" }, + "node_modules/jiti": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", + "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -3069,6 +3467,234 @@ "node": ">= 0.8.0" } }, + "node_modules/lightningcss": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", + "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-darwin-arm64": "1.30.1", + "lightningcss-darwin-x64": "1.30.1", + "lightningcss-freebsd-x64": "1.30.1", + "lightningcss-linux-arm-gnueabihf": "1.30.1", + "lightningcss-linux-arm64-gnu": "1.30.1", + "lightningcss-linux-arm64-musl": "1.30.1", + "lightningcss-linux-x64-gnu": "1.30.1", + "lightningcss-linux-x64-musl": "1.30.1", + "lightningcss-win32-arm64-msvc": "1.30.1", + "lightningcss-win32-x64-msvc": "1.30.1" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz", + "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz", + "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz", + "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz", + "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz", + "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz", + "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz", + "integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz", + "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz", + "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz", + "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", @@ -3240,6 +3866,24 @@ "yallist": "^3.0.2" } }, + "node_modules/lucide-react": { + "version": "0.525.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.525.0.tgz", + "integrity": "sha512-Tm1txJ2OkymCGkvwoHt33Y2JpN5xucVq1slHcgE6Lk0WjDfjgKWor5CdVER8U6DvcfMwh4M8XxmpTiyzfmfDYQ==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -3320,6 +3964,42 @@ "node": "*" } }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", + "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -3344,7 +4024,6 @@ "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, "funding": [ { "type": "github", @@ -3476,7 +4155,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -3509,7 +4187,6 @@ "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "dev": true, "funding": [ { "type": "opencollective", @@ -3741,7 +4418,6 @@ "version": "4.45.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.45.1.tgz", "integrity": "sha512-4iya7Jb76fVpQyLoiVpzUrsjQ12r3dM7fIVz+4NwoYvZOShknRmiv+iu9CClZml5ZLGb0XMcYLutK6w9tgxHDw==", - "dev": true, "license": "MIT", "dependencies": { "@types/estree": "1.0.8" @@ -3893,7 +4569,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -3985,11 +4660,61 @@ "url": "https://opencollective.com/synckit" } }, + "node_modules/tailwind-merge": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz", + "integrity": "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz", + "integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz", + "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tar": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "license": "ISC", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { "version": "0.2.14", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", - "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.4.4", @@ -4006,7 +4731,6 @@ "version": "6.4.6", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", - "dev": true, "license": "MIT", "peerDependencies": { "picomatch": "^3 || ^4" @@ -4021,7 +4745,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -4056,6 +4779,16 @@ "typescript": ">=4.8.4" } }, + "node_modules/tw-animate-css": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.3.5.tgz", + "integrity": "sha512-t3u+0YNoloIhj1mMXs779P6MO9q3p3mvGn4k1n3nJPqJw/glZcuijG2qTSN4z4mgNRfW5ZC3aXJFLwDtiipZXA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Wombosvideo" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -4107,6 +4840,13 @@ "typescript": ">=4.8.4 <5.9.0" } }, + "node_modules/undici-types": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", + "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", + "devOptional": true, + "license": "MIT" + }, "node_modules/update-browserslist-db": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", @@ -4152,7 +4892,6 @@ "version": "7.0.5", "resolved": "https://registry.npmjs.org/vite/-/vite-7.0.5.tgz", "integrity": "sha512-1mncVwJxy2C9ThLwz0+2GKZyEXuC3MyWtAAlNftlZZXZDP3AJt5FmwcMit/IGGaNZ8ZOB2BNO/HFUB+CpN0NQw==", - "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.25.0", @@ -4227,7 +4966,6 @@ "version": "6.4.6", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", - "dev": true, "license": "MIT", "peerDependencies": { "picomatch": "^3 || ^4" @@ -4242,7 +4980,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -4319,7 +5056,7 @@ "version": "2.8.0", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", - "dev": true, + "devOptional": true, "license": "ISC", "bin": { "yaml": "bin.mjs" diff --git a/frontend/package.json b/frontend/package.json index 77e36516..86d75aff 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,17 +14,28 @@ "format:fix": "npm run prettier -- --write" }, "dependencies": { + "@radix-ui/react-slot": "^1.2.3", + "@tailwindcss/vite": "^4.1.11", + "@tanstack/react-query": "^5.83.0", + "@tanstack/react-query-devtools": "^5.83.0", "axios": "^1.10.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "dotenv": "^17.2.0", + "lucide-react": "^0.525.0", "react": "^19.1.0", "react-dom": "^19.1.0", "react-router-dom": "^7.7.0", - "react-toastify": "^11.0.5" + "react-toastify": "^11.0.5", + "tailwind-merge": "^3.3.1", + "tailwindcss": "^4.1.11" }, "devDependencies": { "@eslint/js": "^9.29.0", + "@types/node": "^24.0.15", "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", - "@vitejs/plugin-react": "^4.5.2", + "@vitejs/plugin-react": "^4.7.0", "eslint": "^9.31.0", "eslint-config-prettier": "^10.1.5", "eslint-plugin-prettier": "^5.5.1", @@ -34,6 +45,7 @@ "husky": "^8.0.0", "lint-staged": "^16.1.2", "prettier": "^3.6.2", + "tw-animate-css": "^1.3.5", "typescript": "~5.8.3", "typescript-eslint": "^8.34.1", "vite": "^7.0.0" diff --git a/frontend/src/App.css b/frontend/src/App.css deleted file mode 100644 index b9d355df..00000000 --- a/frontend/src/App.css +++ /dev/null @@ -1,42 +0,0 @@ -#root { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; -} - -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; -} -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); -} -.logo.react:hover { - filter: drop-shadow(0 0 2em #61dafbaa); -} - -@keyframes logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - -@media (prefers-reduced-motion: no-preference) { - a:nth-of-type(2) .logo { - animation: logo-spin infinite 20s linear; - } -} - -.card { - padding: 2em; -} - -.read-the-docs { - color: #888; -} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 4e403b28..e1cff623 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,6 +1,5 @@ -import "./App.css"; -import { UserProvider } from "./context/AuthProvider"; -import Router from "./root/Router"; +import { UserProvider } from './context/AuthProvider'; +import Router from './root/Router'; function App() { return ( diff --git a/frontend/src/assets/coder.svg b/frontend/src/assets/coder.svg new file mode 100644 index 00000000..f056439c --- /dev/null +++ b/frontend/src/assets/coder.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/frontend/src/components/ui/button.tsx b/frontend/src/components/ui/button.tsx new file mode 100644 index 00000000..a2df8dce --- /dev/null +++ b/frontend/src/components/ui/button.tsx @@ -0,0 +1,59 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + { + variants: { + variant: { + default: + "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", + destructive: + "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", + secondary: + "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", + ghost: + "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2 has-[>svg]:px-3", + sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", + lg: "h-10 rounded-md px-6 has-[>svg]:px-4", + icon: "size-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +function Button({ + className, + variant, + size, + asChild = false, + ...props +}: React.ComponentProps<"button"> & + VariantProps & { + asChild?: boolean + }) { + const Comp = asChild ? Slot : "button" + + return ( + + ) +} + +export { Button, buttonVariants } diff --git a/frontend/src/components/ui/card.tsx b/frontend/src/components/ui/card.tsx new file mode 100644 index 00000000..d05bbc6c --- /dev/null +++ b/frontend/src/components/ui/card.tsx @@ -0,0 +1,92 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Card({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardDescription({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardAction({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardAction, + CardDescription, + CardContent, +} diff --git a/frontend/src/context/AuthProvider.tsx b/frontend/src/context/AuthProvider.tsx index 5301cee5..da7b91ad 100644 --- a/frontend/src/context/AuthProvider.tsx +++ b/frontend/src/context/AuthProvider.tsx @@ -1,9 +1,4 @@ -import { - createContext, - useMemo, - useState, - type ReactNode, -} from "react"; +import { createContext, useMemo, useState, type ReactNode } from 'react'; export type User = { githubId: string; @@ -18,19 +13,19 @@ export interface UserContextInterface { } const defaultUser: User = { - githubId: "", - githubUsername: "", - avatarUrl: "", + githubId: '', + githubUsername: '', + avatarUrl: '' }; const defaultState: UserContextInterface = { user: defaultUser, login: () => { - throw new Error("login must be used within UserProvider"); + throw new Error('login must be used within UserProvider'); }, logout: () => { - throw new Error("logout must be used within UserProvider"); - }, + throw new Error('logout must be used within UserProvider'); + } }; export const UserContext = createContext(defaultState); @@ -44,12 +39,12 @@ export const UserProvider = ({ children }: UserProviderProps) => { const login = (newUser: User, token: string) => { setUser(newUser); - localStorage.setItem("token", token); + localStorage.setItem('token', token); }; const logout = () => { setUser(defaultUser); - localStorage.removeItem("token"); + localStorage.removeItem('token'); }; const value = useMemo(() => ({ user, login, logout }), [user]); diff --git a/frontend/src/index.css b/frontend/src/index.css index 08a3ac9e..d0031645 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1,68 +1,190 @@ +@import "http://github.com/tailwindcss"; +@import "http://github.com/tw-animate-css"; + +@custom-variant dark (&:is(.dark *)); + +/* Define custom colors as CSS variables */ +@theme { + --color-vite-blue: #000000; + --color-vite-purple: #535bf2; + --color-vite-dark: #242424; + --color-vite-dark-light: #1a1a1a; + --color-primary-brown: #a0522d; +} + :root { font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; line-height: 1.5; font-weight: 400; - color-scheme: light dark; color: rgba(255, 255, 255, 0.87); background-color: #242424; - font-synthesis: none; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; -} + --radius: 0.625rem; + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); -a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; -} -a:hover { - color: #535bf2; + --cc-light-blue: oklch(0.6686 0.1358 231.66); + --cc-mid-blue:oklch(0.6163 0.140573 239.7492); + --cc-dark-blue:oklch(0.4668 0.1625 256.62); + --cc-app-blue:oklch(0.3876 0.1761 261.76); + --cc-app-gray-background:oklch(0.9585 0.0195 270.21); + --cc-app-orange:oklch(0.7362 0.1641 62.07); } -body { - margin: 0; - display: flex; - place-items: center; - min-width: 320px; - min-height: 100vh; +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } } -h1 { - font-size: 3.2em; - line-height: 1.1; -} +@theme inline { + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-prima --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-acry: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); -button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - background-color: #1a1a1a; - cursor: pointer; - transition: border-color 0.25s; + --color-cc-light-blue: var(--cc-light-blue); + --color-cc-mid-blue: var(--cc-mid-blue); + --color-cc-dark-blue: var(--cc-dark-blue); + --color-cc-app-blue: var(--cc-app-blue); + --color-cc-app-gray-background: var(--cc-app-gray-background); + --color-cc-app-orange: var(--cc-app-orange); } -button:hover { - border-color: #646cff; -} -button:focus, -button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; + +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); } -@media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; +@layer base { + * { + @apply border-border outline-ring/50; } - a:hover { - color: #747bff; + body { + @apply bg-background text-foreground; } - button { - background-color: #f9f9f9; - } -} +} \ No newline at end of file diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts new file mode 100644 index 00000000..bd0c391d --- /dev/null +++ b/frontend/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index bef5202a..cb2764c5 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -1,10 +1,14 @@ -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import './index.css' -import App from './App.tsx' +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import './index.css'; +import App from './App.tsx'; +import { QueryClientProvider } from '@tanstack/react-query'; +import { queryClient } from './utils/react-query.ts'; createRoot(document.getElementById('root')!).render( - - , -) + + + + +); diff --git a/frontend/src/pages/Login/index.tsx b/frontend/src/pages/Login/index.tsx new file mode 100644 index 00000000..7ee300a6 --- /dev/null +++ b/frontend/src/pages/Login/index.tsx @@ -0,0 +1,13 @@ +import type { FC } from 'react'; +import LoginComponent from './loginComponent'; +import AuthLayout from '@/shared/components/AuthLayout'; + +const Login: FC = () => { + return ( + + + + ); +}; + +export default Login; diff --git a/frontend/src/pages/Login/loginComponent.tsx b/frontend/src/pages/Login/loginComponent.tsx new file mode 100644 index 00000000..d60a061b --- /dev/null +++ b/frontend/src/pages/Login/loginComponent.tsx @@ -0,0 +1,45 @@ +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardFooter, CardHeader } from '@/components/ui/card'; +import { type FC } from 'react'; +import Coder from '@/assets/coder.svg'; +import { env } from '@/utils/endpoints'; + +const LoginComponent: FC = () => { + const handleGithubLogin = () => { + window.location.href = env.GithubAuthUrl || ''; + }; + + return ( + + + Developer Illustration + + + + +
+
+ + +

+ No idea where to start? Try this{' '} + + Code Triage + +

+
+
+ ); +}; + +export default LoginComponent; diff --git a/frontend/src/pages/Login/main.tsx b/frontend/src/pages/Login/main.tsx deleted file mode 100644 index 8daae3f3..00000000 --- a/frontend/src/pages/Login/main.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { useContext } from "react"; -import { UserContext } from "../../context/AuthProvider"; - -function Login() { - const { user } = useContext(UserContext); - - return
login page username : {user.githubUsername}
; -} -export default Login; diff --git a/frontend/src/root/PrivateRoutes.tsx b/frontend/src/root/PrivateRoutes.tsx index 27ba9999..feebec93 100644 --- a/frontend/src/root/PrivateRoutes.tsx +++ b/frontend/src/root/PrivateRoutes.tsx @@ -1,18 +1,18 @@ -import { useEffect, type ReactNode } from "react"; -import { Navigate } from "react-router-dom"; -import { toast } from "react-toastify"; -import "react-toastify/dist/ReactToastify.css"; +import { useEffect, type ReactNode } from 'react'; +import { Navigate } from 'react-router-dom'; +import { toast } from 'react-toastify'; +import 'react-toastify/dist/ReactToastify.css'; interface PrivateRoutesProps { children: ReactNode; } const PrivateRoutes: React.FC = ({ children }) => { - const localStorageToken = localStorage.getItem("token"); + const localStorageToken = localStorage.getItem('token'); useEffect(() => { if (!localStorageToken) { - toast.warning("You need to login to proceed!"); + toast.warning('You need to login to proceed!'); } }, [localStorageToken]); diff --git a/frontend/src/root/Router.tsx b/frontend/src/root/Router.tsx index 944e51d4..d74e1983 100644 --- a/frontend/src/root/Router.tsx +++ b/frontend/src/root/Router.tsx @@ -1,6 +1,6 @@ -import { createBrowserRouter, RouterProvider } from "react-router-dom"; -import PrivateRoutes from "./PrivateRoutes"; -import { routesConfig, type RoutesType } from "./routesConfig"; +import { createBrowserRouter, RouterProvider } from 'react-router-dom'; +import PrivateRoutes from './PrivateRoutes'; +import { routesConfig, type RoutesType } from './routesConfig'; const generateRoutes = (routes: RoutesType[]) => { return routes.map(({ path, element, isProtected }) => { diff --git a/frontend/src/root/routeConstants.ts b/frontend/src/root/routeConstants.ts index 96e0b22e..48ed62e8 100644 --- a/frontend/src/root/routeConstants.ts +++ b/frontend/src/root/routeConstants.ts @@ -1,4 +1,4 @@ export const ROUTES = { - LOGIN: "/login", - LANDING: "/", + LOGIN: '/login', + LANDING: '/' }; diff --git a/frontend/src/root/routesConfig.tsx b/frontend/src/root/routesConfig.tsx index 674aaf1a..28f92c5f 100644 --- a/frontend/src/root/routesConfig.tsx +++ b/frontend/src/root/routesConfig.tsx @@ -1,6 +1,6 @@ -import type { ReactNode } from "react"; -import Login from "../pages/Login/main"; -import { ROUTES } from "./routeConstants"; +import type { ReactNode } from 'react'; +import Login from '../pages/Login'; +import { ROUTES } from './routeConstants'; export interface RoutesType { path: string; @@ -12,8 +12,8 @@ export const routesConfig: RoutesType[] = [ { path: ROUTES.LOGIN, element: , - isProtected: false, - }, + isProtected: false + } // { // path: //path, // element: ( diff --git a/frontend/src/shared/components/AuthLayout.tsx b/frontend/src/shared/components/AuthLayout.tsx new file mode 100644 index 00000000..5dd61434 --- /dev/null +++ b/frontend/src/shared/components/AuthLayout.tsx @@ -0,0 +1,87 @@ +import { useEffect, type FC, type ReactNode } from 'react'; +import { useLocation, useNavigate } from 'react-router-dom'; +import { getAccessToken, getUserData } from '../utils/local-storage'; +import { ROUTES } from '@/root/routeConstants'; +import { Card } from '@/components/ui/card'; +import { CheckCircle } from 'lucide-react'; + +interface AuthLayoutProps { + children: ReactNode; +} + +const AuthLayout: FC = ({ children }) => { + const location = useLocation(); + const navigate = useNavigate(); + + useEffect(() => { + const userAccessToken = getAccessToken(); + const userData = getUserData(); + const shouldRedirect = [ROUTES.LOGIN].includes(location.pathname); + + if (userAccessToken && userData && shouldRedirect) { + navigate(ROUTES.LANDING); + } + }, [navigate, location.pathname]); + + return ( +
+
+ +
+
+ {Array.from({ length: 64 }).map((_, i) => ( +
+ ))} +
+
+ +
+
+
+
+

+ Code Curiosity +

+
+ +
+ {[ + 'Earn and Upskill', + 'Set Your Goals', + 'Leader Board', + 'Open Source Contribution' + ].map((text, i) => ( +
+ + + {text} + +
+ ))} +
+
+
+
+ + {/* Bottom Grid Decoration */} +
+
+ {Array.from({ length: 64 }).map((_, i) => ( +
+ ))} +
+
+ + + + {children} + +
+
+ ); +}; + +export default AuthLayout; diff --git a/frontend/src/shared/lib/constants.ts b/frontend/src/shared/lib/constants.ts new file mode 100644 index 00000000..7ed62cd7 --- /dev/null +++ b/frontend/src/shared/lib/constants.ts @@ -0,0 +1,2 @@ +export const ACCESS_TOKEN_KEY = 'accessToken'; +export const USER_DATA_KEY = 'userData'; diff --git a/frontend/src/shared/types/auth.ts b/frontend/src/shared/types/auth.ts new file mode 100644 index 00000000..06c2c744 --- /dev/null +++ b/frontend/src/shared/types/auth.ts @@ -0,0 +1,16 @@ +export interface User { + id: number; + githubId: string; + githubUsername: string; + avatarUrl: string; + email: string | null; + currentActiveGoalId: number | null; + currentBalance: number; + isBlocked: boolean; + isAdmin: boolean; + password: string; + isDeleted: boolean; + DeletedAt: Date | null; + createdAt: Date; + updatedAt: Date; +} diff --git a/frontend/src/shared/utils/local-storage.ts b/frontend/src/shared/utils/local-storage.ts new file mode 100644 index 00000000..4c7be901 --- /dev/null +++ b/frontend/src/shared/utils/local-storage.ts @@ -0,0 +1,29 @@ +import { USER_DATA_KEY, ACCESS_TOKEN_KEY } from '@/shared/lib/constants'; +import type { User } from '../types/auth'; + +export const getUserData = () => { + try { + const userData = localStorage.getItem(USER_DATA_KEY); + return userData ? (JSON.parse(userData) as User) : null; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (error) { + return null; + } +}; + +export const setUserData = (data: User) => { + localStorage.setItem(USER_DATA_KEY, JSON.stringify(data)); +}; + +export const getAccessToken = (): string | null => { + return localStorage.getItem(ACCESS_TOKEN_KEY) || null; +}; + +export const setAccessToken = (token: string) => { + localStorage.setItem(ACCESS_TOKEN_KEY, token); +}; + +export const clearUserCredentials = () => { + localStorage.removeItem(USER_DATA_KEY); + localStorage.removeItem(ACCESS_TOKEN_KEY); +}; diff --git a/frontend/src/utils/axios.ts b/frontend/src/utils/axios.ts index d5f800d9..b612ca65 100644 --- a/frontend/src/utils/axios.ts +++ b/frontend/src/utils/axios.ts @@ -1,17 +1,16 @@ -import axios from "axios"; +import axios from 'axios'; export const api = axios.create({ - baseURL: "http://localhost:8080/api/v1", + baseURL: 'http://localhost:8080/api/v1' }); api.interceptors.request.use( - (config) => { - const token = localStorage.getItem("token"); + config => { + const token = localStorage.getItem('token'); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }, - (error) => Promise.reject(error) - + error => Promise.reject(error) ); diff --git a/frontend/src/utils/endpoints.ts b/frontend/src/utils/endpoints.ts new file mode 100644 index 00000000..6708f2c8 --- /dev/null +++ b/frontend/src/utils/endpoints.ts @@ -0,0 +1,5 @@ +export const env = { + GithubAuthUrl: import.meta.env.VITE_GITHUB_AUTH_URL as string, + BackendUrl: import.meta.env.VITE_BACKEND_URL as string, + ServerBaseUrl: import.meta.env.VITE_SERVER_BASE_URL as string +}; diff --git a/frontend/src/utils/react-query.ts b/frontend/src/utils/react-query.ts new file mode 100644 index 00000000..a97896ce --- /dev/null +++ b/frontend/src/utils/react-query.ts @@ -0,0 +1,11 @@ +import { QueryClient } from '@tanstack/react-query'; + +export const queryClient = new QueryClient({ + defaultOptions: { + queries: { + refetchOnWindowFocus: false, + retry: 1, + staleTime: 1000 * 60 * 5 + } + } +}); diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts index 11f02fe2..776744a6 100644 --- a/frontend/src/vite-env.d.ts +++ b/frontend/src/vite-env.d.ts @@ -1 +1,10 @@ /// + +interface ImportMetaEnv { + readonly VITE_BACKEND_URL: string; + // add other env vars here +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js new file mode 100644 index 00000000..e916a26c --- /dev/null +++ b/frontend/tailwind.config.js @@ -0,0 +1,63 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], + theme: { + extend: { + borderRadius: { + lg: 'var(--radius)', + md: 'calc(var(--radius) - 2px)', + sm: 'calc(var(--radius) - 4px)' + }, + colors: { + cclightblue: 'hsl(var(--cc-light-blue))', + ccmidblue: 'hsl(var(--cc-mid-blue))', + ccdarkblue: 'hsl(var(--cc-dark-blue))', + ccappblue: 'hsl(var(--cc-app-blue))', + ccappgraybackground: 'hsl(var(--cc-app-gray-background))', + ccapporange: 'hsl(var(--cc-app-orange))', + + background: 'hsl(var(--background))', + foreground: 'hsl(var(--foreground))', + card: { + DEFAULT: 'hsl(var(--card))', + foreground: 'hsl(var(--card-foreground))' + }, + popover: { + DEFAULT: 'hsl(var(--popover))', + foreground: 'hsl(var(--popover-foreground))' + }, + primary: { + DEFAULT: 'hsl(var(--primary))', + foreground: 'hsl(var(--primary-foreground))' + }, + secondary: { + DEFAULT: 'hsl(var(--secondary))', + foreground: 'hsl(var(--secondary-foreground))' + }, + muted: { + DEFAULT: 'hsl(var(--muted))', + foreground: 'hsl(var(--muted-foreground))' + }, + accent: { + DEFAULT: 'hsl(var(--accent))', + foreground: 'hsl(var(--accent-foreground))' + }, + destructive: { + DEFAULT: 'hsl(var(--destructive))', + foreground: 'hsl(var(--destructive-foreground))' + }, + border: 'hsl(var(--border))', + input: 'hsl(var(--input))', + ring: 'hsl(var(--ring))', + chart: { + 1: 'hsl(var(--chart-1))', + 2: 'hsl(var(--chart-2))', + 3: 'hsl(var(--chart-3))', + 4: 'hsl(var(--chart-4))', + 5: 'hsl(var(--chart-5))' + } + } + } + }, + plugins: [] +}; diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json index 227a6c67..0b235e3e 100644 --- a/frontend/tsconfig.app.json +++ b/frontend/tsconfig.app.json @@ -6,6 +6,12 @@ "lib": ["ES2022", "DOM", "DOM.Iterable"], "module": "ESNext", "skipLibCheck": true, + "baseUrl": ".", + "paths": { + "@/*": [ + "./src/*" + ] + }, /* Bundler mode */ "moduleResolution": "bundler", diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 1e173931..0747f053 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -8,7 +8,7 @@ "path": "./tsconfig.node.json" } ], - "compilerOptions": { + "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["./src/*"] diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index db008821..e0e1b904 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -1,12 +1,13 @@ import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; +import tailwindcss from '@tailwindcss/vite'; +import path from "path" -// https://vite.dev/config/ export default defineConfig({ - plugins: [react()], + plugins: [react(), tailwindcss()], resolve: { alias: { - '@': '/src' - } - } + "@": path.resolve(__dirname, "./src"), + }, + }, }); diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 57c86129..d1791a55 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -2,7 +2,7 @@ # yarn lockfile v1 -"@ampproject/remapping@^2.2.0": +"@ampproject/remapping@^2.2.0", "@ampproject/remapping@^2.3.0": version "2.3.0" resolved "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz" integrity sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw== @@ -267,6 +267,13 @@ resolved "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz" integrity sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ== +"@isaacs/fs-minipass@^4.0.0": + version "4.0.1" + resolved "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz" + integrity sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w== + dependencies: + minipass "^7.0.4" + "@jridgewell/gen-mapping@^0.3.12", "@jridgewell/gen-mapping@^0.3.5": version "0.3.12" resolved "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz" @@ -319,6 +326,18 @@ resolved "https://registry.npmjs.org/@pkgr/core/-/core-0.2.7.tgz" integrity sha512-YLT9Zo3oNPJoBjBc4q8G2mjU4tqIbf5CEOORbUUr48dCD9q3umJ3IPlVqOqDakPfd2HuwccBaqlGhN4Gmr5OWg== +"@radix-ui/react-compose-refs@1.1.2": + version "1.1.2" + resolved "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz" + integrity sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg== + +"@radix-ui/react-slot@^1.2.3": + version "1.2.3" + resolved "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz" + integrity sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A== + dependencies: + "@radix-ui/react-compose-refs" "1.1.2" + "@rolldown/pluginutils@1.0.0-beta.27": version "1.0.0-beta.27" resolved "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz" @@ -334,6 +353,83 @@ resolved "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.45.1.tgz" integrity sha512-a6WIAp89p3kpNoYStITT9RbTbTnqarU7D8N8F2CV+4Cl9fwCOZraLVuVFvlpsW0SbIiYtEnhCZBPLoNdRkjQFw== +"@tailwindcss/node@4.1.11": + version "4.1.11" + resolved "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.11.tgz" + integrity sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q== + dependencies: + "@ampproject/remapping" "^2.3.0" + enhanced-resolve "^5.18.1" + jiti "^2.4.2" + lightningcss "1.30.1" + magic-string "^0.30.17" + source-map-js "^1.2.1" + tailwindcss "4.1.11" + +"@tailwindcss/oxide-linux-x64-gnu@4.1.11": + version "4.1.11" + resolved "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.11.tgz" + integrity sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg== + +"@tailwindcss/oxide-linux-x64-musl@4.1.11": + version "4.1.11" + resolved "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.11.tgz" + integrity sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q== + +"@tailwindcss/oxide@4.1.11": + version "4.1.11" + resolved "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.11.tgz" + integrity sha512-Q69XzrtAhuyfHo+5/HMgr1lAiPP/G40OMFAnws7xcFEYqcypZmdW8eGXaOUIeOl1dzPJBPENXgbjsOyhg2nkrg== + dependencies: + detect-libc "^2.0.4" + tar "^7.4.3" + optionalDependencies: + "@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.11": + version "4.1.11" + resolved "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.11.tgz" + integrity sha512-RHYhrR3hku0MJFRV+fN2gNbDNEh3dwKvY8XJvTxCSXeMOsCRSr+uKvDWQcbizrHgjML6ZmTE5OwMrl5wKcujCw== + dependencies: + "@tailwindcss/node" "4.1.11" + "@tailwindcss/oxide" "4.1.11" + tailwindcss "4.1.11" + +"@tanstack/query-core@5.83.0": + version "5.83.0" + resolved "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.83.0.tgz" + integrity sha512-0M8dA+amXUkyz5cVUm/B+zSk3xkQAcuXuz5/Q/LveT4ots2rBpPTZOzd7yJa2Utsf8D2Upl5KyjhHRY+9lB/XA== + +"@tanstack/query-devtools@5.81.2": + version "5.81.2" + resolved "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.81.2.tgz" + integrity sha512-jCeJcDCwKfoyyBXjXe9+Lo8aTkavygHHsUHAlxQKKaDeyT0qyQNLKl7+UyqYH2dDF6UN/14873IPBHchcsU+Zg== + +"@tanstack/react-query-devtools@^5.83.0": + version "5.83.0" + resolved "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.83.0.tgz" + integrity sha512-yfp8Uqd3I1jgx8gl0lxbSSESu5y4MO2ThOPBnGNTYs0P+ZFu+E9g5IdOngyUGuo6Uz6Qa7p9TLdZEX3ntik2fQ== + dependencies: + "@tanstack/query-devtools" "5.81.2" + +"@tanstack/react-query@^5.83.0": + version "5.83.0" + resolved "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.83.0.tgz" + integrity sha512-/XGYhZ3foc5H0VM2jLSD/NyBRIOK4q9kfeml4+0x2DlL6xVuAcVEW+hTlTapAmejObg0i3eNqhkr2dT+eciwoQ== + dependencies: + "@tanstack/query-core" "5.83.0" + "@types/babel__core@^7.20.5": version "7.20.5" resolved "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz" @@ -377,12 +473,19 @@ resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz" integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== +"@types/node@^20.19.0 || >=22.12.0", "@types/node@^24.0.15": + version "24.0.15" + resolved "https://registry.npmjs.org/@types/node/-/node-24.0.15.tgz" + integrity sha512-oaeTSbCef7U/z7rDeJA138xpG3NuKc64/rZ2qmUFkFJmnMsAPaluIifqyWd8hSSMxyP9oie3dLAqYPblag9KgA== + dependencies: + undici-types "~7.8.0" + "@types/react-dom@^19.1.6": version "19.1.6" resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.6.tgz" integrity sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw== -"@types/react@^19.0.0", "@types/react@^19.1.8": +"@types/react@*", "@types/react@^19.0.0", "@types/react@^19.1.8": version "19.1.8" resolved "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz" integrity sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g== @@ -487,7 +590,7 @@ "@typescript-eslint/types" "8.37.0" eslint-visitor-keys "^4.2.1" -"@vitejs/plugin-react@^4.5.2": +"@vitejs/plugin-react@^4.7.0": version "4.7.0" resolved "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz" integrity sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA== @@ -635,6 +738,18 @@ chalk@^5.4.1: resolved "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz" integrity sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w== +chownr@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz" + integrity sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g== + +class-variance-authority@^0.7.1: + version "0.7.1" + resolved "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz" + integrity sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg== + dependencies: + clsx "^2.1.1" + cli-cursor@^5.0.0: version "5.0.0" resolved "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz" @@ -730,6 +845,16 @@ delayed-stream@~1.0.0: resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== +detect-libc@^2.0.3, detect-libc@^2.0.4: + version "2.0.4" + resolved "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz" + integrity sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA== + +dotenv@^17.2.0: + version "17.2.0" + resolved "https://registry.npmjs.org/dotenv/-/dotenv-17.2.0.tgz" + integrity sha512-Q4sgBT60gzd0BB0lSyYD3xM4YxrXA9y4uBDof1JNYGzOXrQdQ6yX+7XIAqoFOGQFOTK1D3Hts5OllpxMDZFONQ== + dunder-proto@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz" @@ -749,6 +874,14 @@ emoji-regex@^10.3.0: resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz" integrity sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw== +enhanced-resolve@^5.18.1: + version "5.18.2" + resolved "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz" + integrity sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.2.0" + environment@^1.0.0: version "1.1.0" resolved "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz" @@ -1110,6 +1243,11 @@ gopd@^1.2.0: resolved "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz" integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== +graceful-fs@^4.2.4: + version "4.2.11" + resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + graphemer@^1.4.0: version "1.4.0" resolved "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz" @@ -1201,6 +1339,11 @@ isexe@^2.0.0: resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== +jiti@*, jiti@^2.4.2, jiti@>=1.21.0: + version "2.4.2" + resolved "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz" + integrity sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A== + js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz" @@ -1253,6 +1396,34 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" +lightningcss-linux-x64-gnu@1.30.1: + version "1.30.1" + resolved "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz" + integrity sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw== + +lightningcss-linux-x64-musl@1.30.1: + version "1.30.1" + resolved "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz" + integrity sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ== + +lightningcss@^1.21.0, lightningcss@1.30.1: + version "1.30.1" + resolved "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz" + integrity sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg== + dependencies: + detect-libc "^2.0.3" + optionalDependencies: + lightningcss-darwin-arm64 "1.30.1" + lightningcss-darwin-x64 "1.30.1" + lightningcss-freebsd-x64 "1.30.1" + lightningcss-linux-arm-gnueabihf "1.30.1" + lightningcss-linux-arm64-gnu "1.30.1" + lightningcss-linux-arm64-musl "1.30.1" + lightningcss-linux-x64-gnu "1.30.1" + lightningcss-linux-x64-musl "1.30.1" + lightningcss-win32-arm64-msvc "1.30.1" + lightningcss-win32-x64-msvc "1.30.1" + lilconfig@^3.1.3: version "3.1.3" resolved "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz" @@ -1316,6 +1487,18 @@ lru-cache@^5.1.1: dependencies: yallist "^3.0.2" +lucide-react@^0.525.0: + version "0.525.0" + resolved "https://registry.npmjs.org/lucide-react/-/lucide-react-0.525.0.tgz" + integrity sha512-Tm1txJ2OkymCGkvwoHt33Y2JpN5xucVq1slHcgE6Lk0WjDfjgKWor5CdVER8U6DvcfMwh4M8XxmpTiyzfmfDYQ== + +magic-string@^0.30.17: + version "0.30.17" + resolved "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz" + integrity sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA== + dependencies: + "@jridgewell/sourcemap-codec" "^1.5.0" + math-intrinsics@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz" @@ -1365,6 +1548,23 @@ minimatch@^9.0.4: dependencies: brace-expansion "^2.0.1" +minipass@^7.0.4, minipass@^7.1.2: + version "7.1.2" + resolved "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz" + integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== + +minizlib@^3.0.1: + version "3.0.2" + resolved "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz" + integrity sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA== + dependencies: + minipass "^7.1.2" + +mkdirp@^3.0.1: + version "3.0.1" + resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz" + integrity sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg== + ms@^2.1.3: version "2.1.3" resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" @@ -1535,7 +1735,7 @@ react-toastify@^11.0.5: dependencies: clsx "^2.1.1" -"react@^18 || ^19", react@^19.1.0, react@>=18: +"react@^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react@^18 || ^19", react@^19.1.0, react@>=18: version "19.1.0" resolved "https://registry.npmjs.org/react/-/react-19.1.0.tgz" integrity sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg== @@ -1697,6 +1897,33 @@ synckit@^0.11.7: dependencies: "@pkgr/core" "^0.2.4" +tailwind-merge@^3.3.1: + version "3.3.1" + resolved "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz" + integrity sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g== + +tailwindcss@^4.1.11, tailwindcss@4.1.11: + version "4.1.11" + resolved "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz" + integrity sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA== + +tapable@^2.2.0: + version "2.2.2" + resolved "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz" + integrity sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg== + +tar@^7.4.3: + version "7.4.3" + resolved "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz" + integrity sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw== + dependencies: + "@isaacs/fs-minipass" "^4.0.0" + chownr "^3.0.0" + minipass "^7.1.2" + minizlib "^3.0.1" + mkdirp "^3.0.1" + yallist "^5.0.0" + tinyglobby@^0.2.14: version "0.2.14" resolved "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz" @@ -1717,6 +1944,11 @@ ts-api-utils@^2.1.0: resolved "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz" integrity sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ== +tw-animate-css@^1.3.5: + version "1.3.5" + resolved "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.3.5.tgz" + integrity sha512-t3u+0YNoloIhj1mMXs779P6MO9q3p3mvGn4k1n3nJPqJw/glZcuijG2qTSN4z4mgNRfW5ZC3aXJFLwDtiipZXA== + type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" resolved "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz" @@ -1739,6 +1971,11 @@ typescript@>=4.8.4, "typescript@>=4.8.4 <5.9.0", typescript@~5.8.3: resolved "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz" integrity sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ== +undici-types@~7.8.0: + version "7.8.0" + resolved "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz" + integrity sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw== + update-browserslist-db@^1.1.3: version "1.1.3" resolved "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz" @@ -1754,7 +1991,7 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" -"vite@^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0", vite@^7.0.0: +"vite@^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0", "vite@^5.2.0 || ^6 || ^7", vite@^7.0.0: version "7.0.5" resolved "https://registry.npmjs.org/vite/-/vite-7.0.5.tgz" integrity sha512-1mncVwJxy2C9ThLwz0+2GKZyEXuC3MyWtAAlNftlZZXZDP3AJt5FmwcMit/IGGaNZ8ZOB2BNO/HFUB+CpN0NQw== @@ -1794,6 +2031,11 @@ yallist@^3.0.2: resolved "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz" integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== +yallist@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz" + integrity sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw== + yaml@^2.4.2, yaml@^2.8.0: version "2.8.0" resolved "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz" diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..69cee8fc --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "Code Curiosity - working copy", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} From a381672b8ec8427b669b5ceb61fb8ce07e10ec13 Mon Sep 17 00:00:00 2001 From: VishakhaSainani-Josh Date: Sun, 27 Jul 2025 00:25:41 +0530 Subject: [PATCH 06/36] implement dashboard page --- frontend/.eslintrc.js | 100 +-- frontend/.prettierrc | 31 +- frontend/README.md | 69 -- frontend/components.json | 12 +- frontend/eslint.config.js | 34 +- frontend/package-lock.json | 690 +++++++++++------- frontend/package.json | 9 +- frontend/src/App.tsx | 12 - frontend/src/{utils => api}/axios.ts | 11 +- frontend/src/api/queries/Leaderboard.ts | 38 + frontend/src/api/queries/Overview.ts | 32 + frontend/src/api/queries/RecentActivities.ts | 22 + frontend/src/api/queries/UserBadges.ts | 22 + .../src/api/queries/UserProfileDetails.ts | 22 + frontend/src/{utils => api}/react-query.ts | 2 +- frontend/src/assets/default-profile-pic.svg | 3 + frontend/src/assets/react.svg | 1 - frontend/src/context/AuthProvider.tsx | 53 -- .../Login/components/LoginComponent.tsx | 51 ++ frontend/src/features/Login/index.tsx | 12 + .../src/features/MyContributions/index.tsx | 7 + .../UserDashboard/components/Leaderboard.tsx | 87 +++ .../components/LeaderboardCard.tsx | 46 ++ .../UserDashboard/components/Overview.tsx | 118 +++ .../UserDashboard/components/OverviewCard.tsx | 32 + .../components/RecentActivities.tsx | 89 +++ .../components/UserDashboardComponent.tsx | 17 + frontend/src/features/UserDashboard/index.tsx | 12 + frontend/src/index.css | 58 +- frontend/src/lib/utils.ts | 6 - frontend/src/main.tsx | 22 +- frontend/src/pages/Login/index.tsx | 13 - frontend/src/pages/Login/loginComponent.tsx | 45 -- frontend/src/root/PrivateRoutes.tsx | 22 - frontend/src/root/Router.tsx | 9 +- frontend/src/root/routeConstants.ts | 4 - frontend/src/root/routes-config.tsx | 34 + frontend/src/root/routesConfig.tsx | 25 - frontend/src/shared/HOC/WithAuth.tsx | 29 + frontend/src/shared/components/AuthLayout.tsx | 87 --- .../components/UserDashboard/Navbar.tsx | 30 + .../components/UserDashboard/UserBadges.tsx | 50 ++ .../components/UserDashboard/UserGoals.tsx | 42 ++ .../UserDashboard/UserProfileCard.tsx | 28 + .../UserDashboard/UserProfileDetails.tsx | 62 ++ .../shared/components/common/ActivityCard.tsx | 58 ++ .../src/shared/components/common/Coin.tsx | 9 + .../src/{ => shared}/components/ui/button.tsx | 36 +- .../src/{ => shared}/components/ui/card.tsx | 28 +- .../shared/components/ui/dropdown-menu.tsx | 255 +++++++ .../src/shared/components/ui/progress.tsx | 35 + .../src/shared/components/ui/separator.tsx | 26 + frontend/src/shared/components/ui/sonner.tsx | 23 + frontend/src/shared/constants/endpoints.ts | 3 + .../src/shared/constants/local-storage.ts | 1 + frontend/src/shared/constants/query-keys.ts | 6 + frontend/src/shared/constants/routes.ts | 4 + frontend/src/shared/context/AuthProvider.tsx | 53 ++ frontend/src/shared/layout/AuthLayout.tsx | 86 +++ .../src/shared/layout/UserDashboardLayout.tsx | 24 + frontend/src/shared/lib/constants.ts | 2 - frontend/src/shared/types/api.ts | 4 + frontend/src/shared/types/navbar.ts | 6 + frontend/src/shared/types/types.ts | 58 ++ frontend/src/shared/utils/local-storage.ts | 20 +- frontend/src/shared/utils/tailwindcss.ts | 6 + frontend/src/utils/endpoints.ts | 5 - frontend/tailwind.config.js | 68 +- frontend/tsconfig.app.json | 4 +- frontend/tsconfig.json | 2 +- frontend/vite.config.ts | 14 +- 71 files changed, 2212 insertions(+), 824 deletions(-) delete mode 100644 frontend/README.md delete mode 100644 frontend/src/App.tsx rename frontend/src/{utils => api}/axios.ts (51%) create mode 100644 frontend/src/api/queries/Leaderboard.ts create mode 100644 frontend/src/api/queries/Overview.ts create mode 100644 frontend/src/api/queries/RecentActivities.ts create mode 100644 frontend/src/api/queries/UserBadges.ts create mode 100644 frontend/src/api/queries/UserProfileDetails.ts rename frontend/src/{utils => api}/react-query.ts (76%) create mode 100644 frontend/src/assets/default-profile-pic.svg delete mode 100644 frontend/src/assets/react.svg delete mode 100644 frontend/src/context/AuthProvider.tsx create mode 100644 frontend/src/features/Login/components/LoginComponent.tsx create mode 100644 frontend/src/features/Login/index.tsx create mode 100644 frontend/src/features/MyContributions/index.tsx create mode 100644 frontend/src/features/UserDashboard/components/Leaderboard.tsx create mode 100644 frontend/src/features/UserDashboard/components/LeaderboardCard.tsx create mode 100644 frontend/src/features/UserDashboard/components/Overview.tsx create mode 100644 frontend/src/features/UserDashboard/components/OverviewCard.tsx create mode 100644 frontend/src/features/UserDashboard/components/RecentActivities.tsx create mode 100644 frontend/src/features/UserDashboard/components/UserDashboardComponent.tsx create mode 100644 frontend/src/features/UserDashboard/index.tsx delete mode 100644 frontend/src/lib/utils.ts delete mode 100644 frontend/src/pages/Login/index.tsx delete mode 100644 frontend/src/pages/Login/loginComponent.tsx delete mode 100644 frontend/src/root/PrivateRoutes.tsx delete mode 100644 frontend/src/root/routeConstants.ts create mode 100644 frontend/src/root/routes-config.tsx delete mode 100644 frontend/src/root/routesConfig.tsx create mode 100644 frontend/src/shared/HOC/WithAuth.tsx delete mode 100644 frontend/src/shared/components/AuthLayout.tsx create mode 100644 frontend/src/shared/components/UserDashboard/Navbar.tsx create mode 100644 frontend/src/shared/components/UserDashboard/UserBadges.tsx create mode 100644 frontend/src/shared/components/UserDashboard/UserGoals.tsx create mode 100644 frontend/src/shared/components/UserDashboard/UserProfileCard.tsx create mode 100644 frontend/src/shared/components/UserDashboard/UserProfileDetails.tsx create mode 100644 frontend/src/shared/components/common/ActivityCard.tsx create mode 100644 frontend/src/shared/components/common/Coin.tsx rename frontend/src/{ => shared}/components/ui/button.tsx (70%) rename frontend/src/{ => shared}/components/ui/card.tsx (93%) create mode 100644 frontend/src/shared/components/ui/dropdown-menu.tsx create mode 100644 frontend/src/shared/components/ui/progress.tsx create mode 100644 frontend/src/shared/components/ui/separator.tsx create mode 100644 frontend/src/shared/components/ui/sonner.tsx create mode 100644 frontend/src/shared/constants/endpoints.ts create mode 100644 frontend/src/shared/constants/local-storage.ts create mode 100644 frontend/src/shared/constants/query-keys.ts create mode 100644 frontend/src/shared/constants/routes.ts create mode 100644 frontend/src/shared/context/AuthProvider.tsx create mode 100644 frontend/src/shared/layout/AuthLayout.tsx create mode 100644 frontend/src/shared/layout/UserDashboardLayout.tsx delete mode 100644 frontend/src/shared/lib/constants.ts create mode 100644 frontend/src/shared/types/api.ts create mode 100644 frontend/src/shared/types/navbar.ts create mode 100644 frontend/src/shared/types/types.ts create mode 100644 frontend/src/shared/utils/tailwindcss.ts delete mode 100644 frontend/src/utils/endpoints.ts diff --git a/frontend/.eslintrc.js b/frontend/.eslintrc.js index 74cf8445..01301365 100644 --- a/frontend/.eslintrc.js +++ b/frontend/.eslintrc.js @@ -1,56 +1,56 @@ -import globals from "globals"; import pluginJs from "@eslint/js"; -import tseslint from "typescript-eslint"; -import pluginReact from "eslint-plugin-react"; -import pluginPrettier from "eslint-plugin-prettier"; -import pluginA11y from "eslint-plugin-jsx-a11y"; import pluginImport from "eslint-plugin-import"; +import pluginA11y from "eslint-plugin-jsx-a11y"; +import pluginPrettier from "eslint-plugin-prettier"; +import pluginReact from "eslint-plugin-react"; +import globals from "globals"; +import tseslint from "typescript-eslint"; /** @type {import('eslint').Linter.FlatConfig[]} */ export default [ - { - files: ["**/*.{js,mjs,cjs,ts,jsx,tsx}"], - languageOptions: { - globals: globals.browser, - ecmaVersion: "latest", - sourceType: "module", - parser: tseslint.parser - }, - plugins: { - react: pluginReact, - "@typescript-eslint": tseslint.plugin, - prettier: pluginPrettier, - "jsx-a11y": pluginA11y, - import: pluginImport, - }, - rules: { - ...pluginJs.configs.recommended.rules, - ...tseslint.configs.recommended.rules, - ...pluginReact.configs.recommended.rules, - "prettier/prettier": "error", - "react/display-name": "off", - "jsx-a11y/anchor-is-valid": "off", - "jsx-a11y/label-has-for": "off", - "camelcase": "off", - "func-names": ["error", "never"], - "import/prefer-default-export": "off", - "import/no-anonymous-default-export": "off", - "import/no-extraneous-dependencies": "off", - "no-multi-spaces": "off", - "class-methods-use-this": "off", - "no-class-assign": "off", - "key-spacing": "off", - "lines-between-class-members": "off", - "no-param-reassign": "off", - "consistent-return": "off", - "jsx-a11y/href-no-hash": "off", - "import/no-unresolved": "off", - "no-tabs": "off", - "react/react-in-jsx-scope": "off", - "no-use-before-define": "off", - "@typescript-eslint/no-use-before-define": "error", - "react/jsx-filename-extension": ["error", { "extensions": [".tsx"] }], - "react/prop-types": "off" - }, + { + files: ["**/*.{js,mjs,cjs,ts,jsx,tsx}"], + languageOptions: { + globals: globals.browser, + ecmaVersion: "latest", + sourceType: "module", + parser: tseslint.parser + }, + plugins: { + react: pluginReact, + "@typescript-eslint": tseslint.plugin, + prettier: pluginPrettier, + "jsx-a11y": pluginA11y, + import: pluginImport }, -]; \ No newline at end of file + rules: { + ...pluginJs.configs.recommended.rules, + ...tseslint.configs.recommended.rules, + ...pluginReact.configs.recommended.rules, + "prettier/prettier": "error", + "react/display-name": "off", + "jsx-a11y/anchor-is-valid": "off", + "jsx-a11y/label-has-for": "off", + camelcase: "off", + "func-names": ["error", "never"], + "import/prefer-default-export": "off", + "import/no-anonymous-default-export": "off", + "import/no-extraneous-dependencies": "off", + "no-multi-spaces": "off", + "class-methods-use-this": "off", + "no-class-assign": "off", + "key-spacing": "off", + "lines-between-class-members": "off", + "no-param-reassign": "off", + "consistent-return": "off", + "jsx-a11y/href-no-hash": "off", + "import/no-unresolved": "off", + "no-tabs": "off", + "react/react-in-jsx-scope": "off", + "no-use-before-define": "off", + "@typescript-eslint/no-use-before-define": "error", + "react/jsx-filename-extension": ["error", { extensions: [".tsx"] }], + "react/prop-types": "off" + } + } +]; diff --git a/frontend/.prettierrc b/frontend/.prettierrc index 93c3b9e2..3c92e9e0 100644 --- a/frontend/.prettierrc +++ b/frontend/.prettierrc @@ -1,16 +1,17 @@ { - "semi": true, - "tabWidth": 2, - "printWidth": 85, - "singleQuote": true, - "trailingComma": "none", - "arrowParens": "avoid", - "endOfLine": "lf", - "htmlWhitespaceSensitivity": "css", - "insertPragma": false, - "jsxSingleQuote": false, - "proseWrap": "always", - "quoteProps": "as-needed", - "requirePragma": false, - "useTabs": false -} \ No newline at end of file + "semi": true, + "tabWidth": 2, + "printWidth": 80, + "singleQuote": false, + "trailingComma": "none", + "arrowParens": "avoid", + "endOfLine": "lf", + "htmlWhitespaceSensitivity": "css", + "insertPragma": false, + "jsxSingleQuote": false, + "proseWrap": "always", + "quoteProps": "as-needed", + "requirePragma": false, + "useTabs": false, + "plugins": ["prettier-plugin-tailwindcss"] +} diff --git a/frontend/README.md b/frontend/README.md deleted file mode 100644 index 7959ce42..00000000 --- a/frontend/README.md +++ /dev/null @@ -1,69 +0,0 @@ -# React + TypeScript + Vite - -This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. - -Currently, two official plugins are available: - -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh - -## Expanding the ESLint configuration - -If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: - -```js -export default tseslint.config([ - globalIgnores(['dist']), - { - files: ['**/*.{ts,tsx}'], - extends: [ - // Other configs... - - // Remove tseslint.configs.recommended and replace with this - ...tseslint.configs.recommendedTypeChecked, - // Alternatively, use this for stricter rules - ...tseslint.configs.strictTypeChecked, - // Optionally, add this for stylistic rules - ...tseslint.configs.stylisticTypeChecked, - - // Other configs... - ], - languageOptions: { - parserOptions: { - project: ['./tsconfig.node.json', './tsconfig.app.json'], - tsconfigRootDir: import.meta.dirname, - }, - // other options... - }, - }, -]) -``` - -You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: - -```js -// eslint.config.js -import reactX from 'eslint-plugin-react-x' -import reactDom from 'eslint-plugin-react-dom' - -export default tseslint.config([ - globalIgnores(['dist']), - { - files: ['**/*.{ts,tsx}'], - extends: [ - // Other configs... - // Enable lint rules for React - reactX.configs['recommended-typescript'], - // Enable lint rules for React DOM - reactDom.configs.recommended, - ], - languageOptions: { - parserOptions: { - project: ['./tsconfig.node.json', './tsconfig.app.json'], - tsconfigRootDir: import.meta.dirname, - }, - // other options... - }, - }, -]) -``` diff --git a/frontend/components.json b/frontend/components.json index 1d282e64..c2eba99e 100644 --- a/frontend/components.json +++ b/frontend/components.json @@ -11,11 +11,11 @@ "prefix": "" }, "aliases": { - "components": "@/components", - "utils": "@/lib/utils", - "ui": "@/components/ui", - "lib": "@/lib", - "hooks": "@/hooks" + "components": "@/shared/components", + "utils": "@/shared/utils/tailwindcss", + "ui": "@/shared/components/ui", + "lib": "@/shared/lib", + "hooks": "@/shared/hooks" }, "iconLibrary": "lucide" -} \ No newline at end of file +} diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js index 092408a9..c1b016ba 100644 --- a/frontend/eslint.config.js +++ b/frontend/eslint.config.js @@ -1,28 +1,28 @@ -import js from '@eslint/js' -import globals from 'globals' -import reactHooks from 'eslint-plugin-react-hooks' -import reactRefresh from 'eslint-plugin-react-refresh' -import tseslint from 'typescript-eslint' +import js from "@eslint/js"; +import reactHooks from "eslint-plugin-react-hooks"; +import reactRefresh from "eslint-plugin-react-refresh"; +import globals from "globals"; +import tseslint from "typescript-eslint"; export default tseslint.config( - { ignores: ['dist'] }, + { ignores: ["dist"] }, { extends: [js.configs.recommended, ...tseslint.configs.recommended], - files: ['**/*.{ts,tsx}'], + files: ["**/*.{ts,tsx}"], languageOptions: { ecmaVersion: 2020, - globals: globals.browser, + globals: globals.browser }, plugins: { - 'react-hooks': reactHooks, - 'react-refresh': reactRefresh, + "react-hooks": reactHooks, + "react-refresh": reactRefresh }, rules: { ...reactHooks.configs.recommended.rules, - 'react-refresh/only-export-components': [ - 'warn', - { allowConstantExport: true }, - ], - }, - }, -) + "react-refresh/only-export-components": [ + "warn", + { allowConstantExport: true } + ] + } + } +); diff --git a/frontend/package-lock.json b/frontend/package-lock.json index f66cef2c..8ead3f33 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,6 +8,8 @@ "name": "code-curiosity-frontend", "version": "0.0.0", "dependencies": { + "@radix-ui/react-progress": "^1.1.7", + "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slot": "^1.2.3", "@tailwindcss/vite": "^4.1.11", "@tanstack/react-query": "^5.83.0", @@ -15,17 +17,20 @@ "axios": "^1.10.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "date-fns": "^4.1.0", "dotenv": "^17.2.0", "lucide-react": "^0.525.0", + "next-themes": "^0.4.6", "react": "^19.1.0", "react-dom": "^19.1.0", "react-router-dom": "^7.7.0", - "react-toastify": "^11.0.5", + "sonner": "^2.0.6", "tailwind-merge": "^3.3.1", "tailwindcss": "^4.1.11" }, "devDependencies": { "@eslint/js": "^9.29.0", + "@types/date-fns": "^2.5.3", "@types/node": "^24.0.15", "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", @@ -39,6 +44,7 @@ "husky": "^8.0.0", "lint-staged": "^16.1.2", "prettier": "^3.6.2", + "prettier-plugin-tailwindcss": "^0.6.14", "tw-animate-css": "^1.3.5", "typescript": "~5.8.3", "typescript-eslint": "^8.34.1", @@ -231,14 +237,14 @@ } }, "node_modules/@babel/helpers": { - "version": "7.27.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz", - "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==", + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.2.tgz", + "integrity": "sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw==", "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.27.2", - "@babel/types": "^7.27.6" + "@babel/types": "^7.28.2" }, "engines": { "node": ">=6.9.0" @@ -327,9 +333,9 @@ } }, "node_modules/@babel/types": { - "version": "7.28.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.1.tgz", - "integrity": "sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ==", + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", + "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", "dev": true, "license": "MIT", "dependencies": { @@ -341,9 +347,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.6.tgz", - "integrity": "sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz", + "integrity": "sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==", "cpu": [ "ppc64" ], @@ -357,9 +363,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.6.tgz", - "integrity": "sha512-S8ToEOVfg++AU/bHwdksHNnyLyVM+eMVAOf6yRKFitnwnbwwPNqKr3srzFRe7nzV69RQKb5DgchIX5pt3L53xg==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.8.tgz", + "integrity": "sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==", "cpu": [ "arm" ], @@ -373,9 +379,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.6.tgz", - "integrity": "sha512-hd5zdUarsK6strW+3Wxi5qWws+rJhCCbMiC9QZyzoxfk5uHRIE8T287giQxzVpEvCwuJ9Qjg6bEjcRJcgfLqoA==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.8.tgz", + "integrity": "sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==", "cpu": [ "arm64" ], @@ -389,9 +395,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.6.tgz", - "integrity": "sha512-0Z7KpHSr3VBIO9A/1wcT3NTy7EB4oNC4upJ5ye3R7taCc2GUdeynSLArnon5G8scPwaU866d3H4BCrE5xLW25A==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.8.tgz", + "integrity": "sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==", "cpu": [ "x64" ], @@ -405,9 +411,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.6.tgz", - "integrity": "sha512-FFCssz3XBavjxcFxKsGy2DYK5VSvJqa6y5HXljKzhRZ87LvEi13brPrf/wdyl/BbpbMKJNOr1Sd0jtW4Ge1pAA==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.8.tgz", + "integrity": "sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==", "cpu": [ "arm64" ], @@ -421,9 +427,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.6.tgz", - "integrity": "sha512-GfXs5kry/TkGM2vKqK2oyiLFygJRqKVhawu3+DOCk7OxLy/6jYkWXhlHwOoTb0WqGnWGAS7sooxbZowy+pK9Yg==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.8.tgz", + "integrity": "sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==", "cpu": [ "x64" ], @@ -437,9 +443,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.6.tgz", - "integrity": "sha512-aoLF2c3OvDn2XDTRvn8hN6DRzVVpDlj2B/F66clWd/FHLiHaG3aVZjxQX2DYphA5y/evbdGvC6Us13tvyt4pWg==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.8.tgz", + "integrity": "sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==", "cpu": [ "arm64" ], @@ -453,9 +459,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.6.tgz", - "integrity": "sha512-2SkqTjTSo2dYi/jzFbU9Plt1vk0+nNg8YC8rOXXea+iA3hfNJWebKYPs3xnOUf9+ZWhKAaxnQNUf2X9LOpeiMQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.8.tgz", + "integrity": "sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==", "cpu": [ "x64" ], @@ -469,9 +475,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.6.tgz", - "integrity": "sha512-SZHQlzvqv4Du5PrKE2faN0qlbsaW/3QQfUUc6yO2EjFcA83xnwm91UbEEVx4ApZ9Z5oG8Bxz4qPE+HFwtVcfyw==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.8.tgz", + "integrity": "sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==", "cpu": [ "arm" ], @@ -485,9 +491,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.6.tgz", - "integrity": "sha512-b967hU0gqKd9Drsh/UuAm21Khpoh6mPBSgz8mKRq4P5mVK8bpA+hQzmm/ZwGVULSNBzKdZPQBRT3+WuVavcWsQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.8.tgz", + "integrity": "sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==", "cpu": [ "arm64" ], @@ -501,9 +507,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.6.tgz", - "integrity": "sha512-aHWdQ2AAltRkLPOsKdi3xv0mZ8fUGPdlKEjIEhxCPm5yKEThcUjHpWB1idN74lfXGnZ5SULQSgtr5Qos5B0bPw==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.8.tgz", + "integrity": "sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==", "cpu": [ "ia32" ], @@ -517,9 +523,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.6.tgz", - "integrity": "sha512-VgKCsHdXRSQ7E1+QXGdRPlQ/e08bN6WMQb27/TMfV+vPjjTImuT9PmLXupRlC90S1JeNNW5lzkAEO/McKeJ2yg==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.8.tgz", + "integrity": "sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==", "cpu": [ "loong64" ], @@ -533,9 +539,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.6.tgz", - "integrity": "sha512-WViNlpivRKT9/py3kCmkHnn44GkGXVdXfdc4drNmRl15zVQ2+D2uFwdlGh6IuK5AAnGTo2qPB1Djppj+t78rzw==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.8.tgz", + "integrity": "sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==", "cpu": [ "mips64el" ], @@ -549,9 +555,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.6.tgz", - "integrity": "sha512-wyYKZ9NTdmAMb5730I38lBqVu6cKl4ZfYXIs31Baf8aoOtB4xSGi3THmDYt4BTFHk7/EcVixkOV2uZfwU3Q2Jw==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.8.tgz", + "integrity": "sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==", "cpu": [ "ppc64" ], @@ -565,9 +571,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.6.tgz", - "integrity": "sha512-KZh7bAGGcrinEj4qzilJ4hqTY3Dg2U82c8bv+e1xqNqZCrCyc+TL9AUEn5WGKDzm3CfC5RODE/qc96OcbIe33w==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.8.tgz", + "integrity": "sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==", "cpu": [ "riscv64" ], @@ -581,9 +587,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.6.tgz", - "integrity": "sha512-9N1LsTwAuE9oj6lHMyyAM+ucxGiVnEqUdp4v7IaMmrwb06ZTEVCIs3oPPplVsnjPfyjmxwHxHMF8b6vzUVAUGw==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.8.tgz", + "integrity": "sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==", "cpu": [ "s390x" ], @@ -597,9 +603,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.6.tgz", - "integrity": "sha512-A6bJB41b4lKFWRKNrWoP2LHsjVzNiaurf7wyj/XtFNTsnPuxwEBWHLty+ZE0dWBKuSK1fvKgrKaNjBS7qbFKig==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.8.tgz", + "integrity": "sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==", "cpu": [ "x64" ], @@ -613,9 +619,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.6.tgz", - "integrity": "sha512-IjA+DcwoVpjEvyxZddDqBY+uJ2Snc6duLpjmkXm/v4xuS3H+3FkLZlDm9ZsAbF9rsfP3zeA0/ArNDORZgrxR/Q==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.8.tgz", + "integrity": "sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==", "cpu": [ "arm64" ], @@ -629,9 +635,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.6.tgz", - "integrity": "sha512-dUXuZr5WenIDlMHdMkvDc1FAu4xdWixTCRgP7RQLBOkkGgwuuzaGSYcOpW4jFxzpzL1ejb8yF620UxAqnBrR9g==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.8.tgz", + "integrity": "sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==", "cpu": [ "x64" ], @@ -645,9 +651,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.6.tgz", - "integrity": "sha512-l8ZCvXP0tbTJ3iaqdNf3pjaOSd5ex/e6/omLIQCVBLmHTlfXW3zAxQ4fnDmPLOB1x9xrcSi/xtCWFwCZRIaEwg==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.8.tgz", + "integrity": "sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==", "cpu": [ "arm64" ], @@ -661,9 +667,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.6.tgz", - "integrity": "sha512-hKrmDa0aOFOr71KQ/19JC7az1P0GWtCN1t2ahYAf4O007DHZt/dW8ym5+CUdJhQ/qkZmI1HAF8KkJbEFtCL7gw==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.8.tgz", + "integrity": "sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==", "cpu": [ "x64" ], @@ -677,9 +683,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.6.tgz", - "integrity": "sha512-+SqBcAWoB1fYKmpWoQP4pGtx+pUUC//RNYhFdbcSA16617cchuryuhOCRpPsjCblKukAckWsV+aQ3UKT/RMPcA==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.8.tgz", + "integrity": "sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==", "cpu": [ "arm64" ], @@ -693,9 +699,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.6.tgz", - "integrity": "sha512-dyCGxv1/Br7MiSC42qinGL8KkG4kX0pEsdb0+TKhmJZgCUDBGmyo1/ArCjNGiOLiIAgdbWgmWgib4HoCi5t7kA==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.8.tgz", + "integrity": "sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==", "cpu": [ "x64" ], @@ -709,9 +715,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.6.tgz", - "integrity": "sha512-42QOgcZeZOvXfsCBJF5Afw73t4veOId//XD3i+/9gSkhSV6Gk3VPlWncctI+JcOyERv85FUo7RxuxGy+z8A43Q==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.8.tgz", + "integrity": "sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==", "cpu": [ "arm64" ], @@ -725,9 +731,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.6.tgz", - "integrity": "sha512-4AWhgXmDuYN7rJI6ORB+uU9DHLq/erBbuMoAuB4VWJTu5KtCgcKYPynF0YI1VkBNuEfjNlLrFr9KZPJzrtLkrQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.8.tgz", + "integrity": "sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==", "cpu": [ "ia32" ], @@ -741,9 +747,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.6.tgz", - "integrity": "sha512-NgJPHHbEpLQgDH2MjQu90pzW/5vvXIZ7KOnPyNBm92A6WgZ/7b6fJyUBjoumLqeOQQGqY2QjQxRo97ah4Sj0cA==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.8.tgz", + "integrity": "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==", "cpu": [ "x64" ], @@ -874,9 +880,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.31.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.31.0.tgz", - "integrity": "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==", + "version": "9.32.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.32.0.tgz", + "integrity": "sha512-BBpRFZK3eX6uMLKz8WxFOBIFFcGFJ/g8XuwjTHCqHROSIsopI+ddn/d5Cfh36+7+e5edVS8dbSHnBNhrLEX0zg==", "dev": true, "license": "MIT", "engines": { @@ -897,9 +903,9 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.3.tgz", - "integrity": "sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz", + "integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1062,9 +1068,9 @@ } }, "node_modules/@pkgr/core": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.7.tgz", - "integrity": "sha512-YLT9Zo3oNPJoBjBc4q8G2mjU4tqIbf5CEOORbUUr48dCD9q3umJ3IPlVqOqDakPfd2HuwccBaqlGhN4Gmr5OWg==", + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", "dev": true, "license": "MIT", "engines": { @@ -1089,6 +1095,91 @@ } } }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.7.tgz", + "integrity": "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz", + "integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-slot": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", @@ -1734,6 +1825,13 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/date-fns": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/@types/date-fns/-/date-fns-2.5.3.tgz", + "integrity": "sha512-4KVPD3g5RjSgZtdOjvI/TDFkLNUHhdoWxmierdQbDeEg17Rov0hbBYtIzNaQA67ORpteOhvR9YEMTb6xeDCang==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1748,9 +1846,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.0.15", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.15.tgz", - "integrity": "sha512-oaeTSbCef7U/z7rDeJA138xpG3NuKc64/rZ2qmUFkFJmnMsAPaluIifqyWd8hSSMxyP9oie3dLAqYPblag9KgA==", + "version": "24.1.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz", + "integrity": "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==", "devOptional": true, "license": "MIT", "dependencies": { @@ -1771,24 +1869,24 @@ "version": "19.1.6", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.6.tgz", "integrity": "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==", - "dev": true, + "devOptional": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.0.0" } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.37.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.37.0.tgz", - "integrity": "sha512-jsuVWeIkb6ggzB+wPCsR4e6loj+rM72ohW6IBn2C+5NCvfUVY8s33iFPySSVXqtm5Hu29Ne/9bnA0JmyLmgenA==", + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.38.0.tgz", + "integrity": "sha512-CPoznzpuAnIOl4nhj4tRr4gIPj5AfKgkiJmGQDaq+fQnRJTYlcBjbX3wbciGmpoPf8DREufuPRe1tNMZnGdanA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.37.0", - "@typescript-eslint/type-utils": "8.37.0", - "@typescript-eslint/utils": "8.37.0", - "@typescript-eslint/visitor-keys": "8.37.0", + "@typescript-eslint/scope-manager": "8.38.0", + "@typescript-eslint/type-utils": "8.38.0", + "@typescript-eslint/utils": "8.38.0", + "@typescript-eslint/visitor-keys": "8.38.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -1802,7 +1900,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.37.0", + "@typescript-eslint/parser": "^8.38.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } @@ -1818,16 +1916,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.37.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.37.0.tgz", - "integrity": "sha512-kVIaQE9vrN9RLCQMQ3iyRlVJpTiDUY6woHGb30JDkfJErqrQEmtdWH3gV0PBAfGZgQXoqzXOO0T3K6ioApbbAA==", + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.38.0.tgz", + "integrity": "sha512-Zhy8HCvBUEfBECzIl1PKqF4p11+d0aUJS1GeUiuqK9WmOug8YCmC4h4bjyBvMyAMI9sbRczmrYL5lKg/YMbrcQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.37.0", - "@typescript-eslint/types": "8.37.0", - "@typescript-eslint/typescript-estree": "8.37.0", - "@typescript-eslint/visitor-keys": "8.37.0", + "@typescript-eslint/scope-manager": "8.38.0", + "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/typescript-estree": "8.38.0", + "@typescript-eslint/visitor-keys": "8.38.0", "debug": "^4.3.4" }, "engines": { @@ -1843,14 +1941,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.37.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.37.0.tgz", - "integrity": "sha512-BIUXYsbkl5A1aJDdYJCBAo8rCEbAvdquQ8AnLb6z5Lp1u3x5PNgSSx9A/zqYc++Xnr/0DVpls8iQ2cJs/izTXA==", + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.38.0.tgz", + "integrity": "sha512-dbK7Jvqcb8c9QfH01YB6pORpqX1mn5gDZc9n63Ak/+jD67oWXn3Gs0M6vddAN+eDXBCS5EmNWzbSxsn9SzFWWg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.37.0", - "@typescript-eslint/types": "^8.37.0", + "@typescript-eslint/tsconfig-utils": "^8.38.0", + "@typescript-eslint/types": "^8.38.0", "debug": "^4.3.4" }, "engines": { @@ -1865,14 +1963,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.37.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.37.0.tgz", - "integrity": "sha512-0vGq0yiU1gbjKob2q691ybTg9JX6ShiVXAAfm2jGf3q0hdP6/BruaFjL/ManAR/lj05AvYCH+5bbVo0VtzmjOA==", + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.38.0.tgz", + "integrity": "sha512-WJw3AVlFFcdT9Ri1xs/lg8LwDqgekWXWhH3iAF+1ZM+QPd7oxQ6jvtW/JPwzAScxitILUIFs0/AnQ/UWHzbATQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.37.0", - "@typescript-eslint/visitor-keys": "8.37.0" + "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/visitor-keys": "8.38.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1883,9 +1981,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.37.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.37.0.tgz", - "integrity": "sha512-1/YHvAVTimMM9mmlPvTec9NP4bobA1RkDbMydxG8omqwJJLEW/Iy2C4adsAESIXU3WGLXFHSZUU+C9EoFWl4Zg==", + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.38.0.tgz", + "integrity": "sha512-Lum9RtSE3EroKk/bYns+sPOodqb2Fv50XOl/gMviMKNvanETUuUcC9ObRbzrJ4VSd2JalPqgSAavwrPiPvnAiQ==", "dev": true, "license": "MIT", "engines": { @@ -1900,15 +1998,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.37.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.37.0.tgz", - "integrity": "sha512-SPkXWIkVZxhgwSwVq9rqj/4VFo7MnWwVaRNznfQDc/xPYHjXnPfLWn+4L6FF1cAz6e7dsqBeMawgl7QjUMj4Ow==", + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.38.0.tgz", + "integrity": "sha512-c7jAvGEZVf0ao2z+nnz8BUaHZD09Agbh+DY7qvBQqLiz8uJzRgVPj5YvOh8I8uEiH8oIUGIfHzMwUcGVco/SJg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.37.0", - "@typescript-eslint/typescript-estree": "8.37.0", - "@typescript-eslint/utils": "8.37.0", + "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/typescript-estree": "8.38.0", + "@typescript-eslint/utils": "8.38.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -1925,9 +2023,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.37.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.37.0.tgz", - "integrity": "sha512-ax0nv7PUF9NOVPs+lmQ7yIE7IQmAf8LGcXbMvHX5Gm+YJUYNAl340XkGnrimxZ0elXyoQJuN5sbg6C4evKA4SQ==", + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.38.0.tgz", + "integrity": "sha512-wzkUfX3plUqij4YwWaJyqhiPE5UCRVlFpKn1oCRn2O1bJ592XxWJj8ROQ3JD5MYXLORW84063z3tZTb/cs4Tyw==", "dev": true, "license": "MIT", "engines": { @@ -1939,16 +2037,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.37.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.37.0.tgz", - "integrity": "sha512-zuWDMDuzMRbQOM+bHyU4/slw27bAUEcKSKKs3hcv2aNnc/tvE/h7w60dwVw8vnal2Pub6RT1T7BI8tFZ1fE+yg==", + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.38.0.tgz", + "integrity": "sha512-fooELKcAKzxux6fA6pxOflpNS0jc+nOQEEOipXFNjSlBS6fqrJOVY/whSn70SScHrcJ2LDsxWrneFoWYSVfqhQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.37.0", - "@typescript-eslint/tsconfig-utils": "8.37.0", - "@typescript-eslint/types": "8.37.0", - "@typescript-eslint/visitor-keys": "8.37.0", + "@typescript-eslint/project-service": "8.38.0", + "@typescript-eslint/tsconfig-utils": "8.38.0", + "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/visitor-keys": "8.38.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -2007,16 +2105,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.37.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.37.0.tgz", - "integrity": "sha512-TSFvkIW6gGjN2p6zbXo20FzCABbyUAuq6tBvNRGsKdsSQ6a7rnV6ADfZ7f4iI3lIiXc4F4WWvtUfDw9CJ9pO5A==", + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.38.0.tgz", + "integrity": "sha512-hHcMA86Hgt+ijJlrD8fX0j1j8w4C92zue/8LOPAFioIno+W0+L7KqE8QZKCcPGc/92Vs9x36w/4MPTJhqXdyvg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.37.0", - "@typescript-eslint/types": "8.37.0", - "@typescript-eslint/typescript-estree": "8.37.0" + "@typescript-eslint/scope-manager": "8.38.0", + "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/typescript-estree": "8.38.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2031,13 +2129,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.37.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.37.0.tgz", - "integrity": "sha512-YzfhzcTnZVPiLfP/oeKtDp2evwvHLMe0LOy7oe+hb9KKIumLNohYS9Hgp1ifwpu42YWxhZE8yieggz6JpqO/1w==", + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.38.0.tgz", + "integrity": "sha512-pWrTcoFNWuwHlA9CvlfSsGWs14JxfN1TH25zM5L7o0pRLhsoZkDnTsXfQRJBEWJoV5DL0jf+Z+sxiud+K0mq1g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.37.0", + "@typescript-eslint/types": "8.38.0", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -2168,13 +2266,13 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz", - "integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", + "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", + "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, @@ -2460,6 +2558,16 @@ "devOptional": true, "license": "MIT" }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/debug": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", @@ -2504,9 +2612,9 @@ } }, "node_modules/dotenv": { - "version": "17.2.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.0.tgz", - "integrity": "sha512-Q4sgBT60gzd0BB0lSyYD3xM4YxrXA9y4uBDof1JNYGzOXrQdQ6yX+7XIAqoFOGQFOTK1D3Hts5OllpxMDZFONQ==", + "version": "17.2.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.1.tgz", + "integrity": "sha512-kQhDYKZecqnM0fCnzI5eIv5L4cAe/iRI+HqMbO/hbRdTAeXDG+M9FjipUxNfbARuEg4iHIbhnhs78BCHNbSxEQ==", "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -2530,9 +2638,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.187", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.187.tgz", - "integrity": "sha512-cl5Jc9I0KGUoOoSbxvTywTa40uspGJt/BDBoDLoxJRSBpWh4FFXBsjNRHfQrONsV/OoEjDfHUmZQa2d6Ze4YgA==", + "version": "1.5.191", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.191.tgz", + "integrity": "sha512-xcwe9ELcuxYLUFqZZxL19Z6HVKcvNkIwhbHUz7L3us6u12yR+7uY89dSl570f/IqNthx8dAw3tojG7i4Ni4tDA==", "dev": true, "license": "ISC" }, @@ -2615,9 +2723,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.6.tgz", - "integrity": "sha512-GVuzuUwtdsghE3ocJ9Bs8PNoF13HNQ5TXbEi2AhvVb8xU1Iwt9Fos9FEamfoee+u/TOsn7GUWc04lz46n2bbTg==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.8.tgz", + "integrity": "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==", "hasInstallScript": true, "license": "MIT", "bin": { @@ -2627,32 +2735,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.6", - "@esbuild/android-arm": "0.25.6", - "@esbuild/android-arm64": "0.25.6", - "@esbuild/android-x64": "0.25.6", - "@esbuild/darwin-arm64": "0.25.6", - "@esbuild/darwin-x64": "0.25.6", - "@esbuild/freebsd-arm64": "0.25.6", - "@esbuild/freebsd-x64": "0.25.6", - "@esbuild/linux-arm": "0.25.6", - "@esbuild/linux-arm64": "0.25.6", - "@esbuild/linux-ia32": "0.25.6", - "@esbuild/linux-loong64": "0.25.6", - "@esbuild/linux-mips64el": "0.25.6", - "@esbuild/linux-ppc64": "0.25.6", - "@esbuild/linux-riscv64": "0.25.6", - "@esbuild/linux-s390x": "0.25.6", - "@esbuild/linux-x64": "0.25.6", - "@esbuild/netbsd-arm64": "0.25.6", - "@esbuild/netbsd-x64": "0.25.6", - "@esbuild/openbsd-arm64": "0.25.6", - "@esbuild/openbsd-x64": "0.25.6", - "@esbuild/openharmony-arm64": "0.25.6", - "@esbuild/sunos-x64": "0.25.6", - "@esbuild/win32-arm64": "0.25.6", - "@esbuild/win32-ia32": "0.25.6", - "@esbuild/win32-x64": "0.25.6" + "@esbuild/aix-ppc64": "0.25.8", + "@esbuild/android-arm": "0.25.8", + "@esbuild/android-arm64": "0.25.8", + "@esbuild/android-x64": "0.25.8", + "@esbuild/darwin-arm64": "0.25.8", + "@esbuild/darwin-x64": "0.25.8", + "@esbuild/freebsd-arm64": "0.25.8", + "@esbuild/freebsd-x64": "0.25.8", + "@esbuild/linux-arm": "0.25.8", + "@esbuild/linux-arm64": "0.25.8", + "@esbuild/linux-ia32": "0.25.8", + "@esbuild/linux-loong64": "0.25.8", + "@esbuild/linux-mips64el": "0.25.8", + "@esbuild/linux-ppc64": "0.25.8", + "@esbuild/linux-riscv64": "0.25.8", + "@esbuild/linux-s390x": "0.25.8", + "@esbuild/linux-x64": "0.25.8", + "@esbuild/netbsd-arm64": "0.25.8", + "@esbuild/netbsd-x64": "0.25.8", + "@esbuild/openbsd-arm64": "0.25.8", + "@esbuild/openbsd-x64": "0.25.8", + "@esbuild/openharmony-arm64": "0.25.8", + "@esbuild/sunos-x64": "0.25.8", + "@esbuild/win32-arm64": "0.25.8", + "@esbuild/win32-ia32": "0.25.8", + "@esbuild/win32-x64": "0.25.8" } }, "node_modules/escalade": { @@ -2679,9 +2787,9 @@ } }, "node_modules/eslint": { - "version": "9.31.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.31.0.tgz", - "integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==", + "version": "9.32.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.32.0.tgz", + "integrity": "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==", "dev": true, "license": "MIT", "dependencies": { @@ -2691,8 +2799,8 @@ "@eslint/config-helpers": "^0.3.0", "@eslint/core": "^0.15.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.31.0", - "@eslint/plugin-kit": "^0.3.1", + "@eslint/js": "9.32.0", + "@eslint/plugin-kit": "^0.3.4", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -2740,9 +2848,9 @@ } }, "node_modules/eslint-config-prettier": { - "version": "10.1.5", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.5.tgz", - "integrity": "sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw==", + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", "bin": { @@ -2756,9 +2864,9 @@ } }, "node_modules/eslint-plugin-prettier": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.1.tgz", - "integrity": "sha512-dobTkHT6XaEVOo8IO90Q4DOSxnm3Y151QxPJlM/vKC0bVy+d6cVWQZLlFiuZPP0wS6vZwSKeJgKkcS+KfMBlRw==", + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.3.tgz", + "integrity": "sha512-NAdMYww51ehKfDyDhv59/eIItUVzU0Io9H2E8nHNGKEeeqlnci+1gCvrHib6EmZdf6GxF+LCV5K7UC65Ezvw7w==", "dev": true, "license": "MIT", "dependencies": { @@ -3368,9 +3476,9 @@ "license": "ISC" }, "node_modules/jiti": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", - "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz", + "integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==", "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" @@ -4045,6 +4153,16 @@ "dev": true, "license": "MIT" }, + "node_modules/next-themes": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", + "integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" + } + }, "node_modules/node-releases": { "version": "2.0.19", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", @@ -4250,6 +4368,93 @@ "node": ">=6.0.0" } }, + "node_modules/prettier-plugin-tailwindcss": { + "version": "0.6.14", + "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.14.tgz", + "integrity": "sha512-pi2e/+ZygeIqntN+vC573BcW5Cve8zUB0SSAGxqpB4f96boZF4M3phPVoOFCeypwkpRYdi7+jQ5YJJUwrkGUAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.21.3" + }, + "peerDependencies": { + "@ianvs/prettier-plugin-sort-imports": "*", + "@prettier/plugin-hermes": "*", + "@prettier/plugin-oxc": "*", + "@prettier/plugin-pug": "*", + "@shopify/prettier-plugin-liquid": "*", + "@trivago/prettier-plugin-sort-imports": "*", + "@zackad/prettier-plugin-twig": "*", + "prettier": "^3.0", + "prettier-plugin-astro": "*", + "prettier-plugin-css-order": "*", + "prettier-plugin-import-sort": "*", + "prettier-plugin-jsdoc": "*", + "prettier-plugin-marko": "*", + "prettier-plugin-multiline-arrays": "*", + "prettier-plugin-organize-attributes": "*", + "prettier-plugin-organize-imports": "*", + "prettier-plugin-sort-imports": "*", + "prettier-plugin-style-order": "*", + "prettier-plugin-svelte": "*" + }, + "peerDependenciesMeta": { + "@ianvs/prettier-plugin-sort-imports": { + "optional": true + }, + "@prettier/plugin-hermes": { + "optional": true + }, + "@prettier/plugin-oxc": { + "optional": true + }, + "@prettier/plugin-pug": { + "optional": true + }, + "@shopify/prettier-plugin-liquid": { + "optional": true + }, + "@trivago/prettier-plugin-sort-imports": { + "optional": true + }, + "@zackad/prettier-plugin-twig": { + "optional": true + }, + "prettier-plugin-astro": { + "optional": true + }, + "prettier-plugin-css-order": { + "optional": true + }, + "prettier-plugin-import-sort": { + "optional": true + }, + "prettier-plugin-jsdoc": { + "optional": true + }, + "prettier-plugin-marko": { + "optional": true + }, + "prettier-plugin-multiline-arrays": { + "optional": true + }, + "prettier-plugin-organize-attributes": { + "optional": true + }, + "prettier-plugin-organize-imports": { + "optional": true + }, + "prettier-plugin-sort-imports": { + "optional": true + }, + "prettier-plugin-style-order": { + "optional": true + }, + "prettier-plugin-svelte": { + "optional": true + } + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -4319,9 +4524,9 @@ } }, "node_modules/react-router": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.7.0.tgz", - "integrity": "sha512-3FUYSwlvB/5wRJVTL/aavqHmfUKe0+Xm9MllkYgGo9eDwNdkvwlJGjpPxono1kCycLt6AnDTgjmXvK3/B4QGuw==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.7.1.tgz", + "integrity": "sha512-jVKHXoWRIsD/qS6lvGveckwb862EekvapdHJN/cGmzw40KnJH5gg53ujOJ4qX6EKIK9LSBfFed/xiQ5yeXNrUA==", "license": "MIT", "dependencies": { "cookie": "^1.0.1", @@ -4341,12 +4546,12 @@ } }, "node_modules/react-router-dom": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.7.0.tgz", - "integrity": "sha512-wwGS19VkNBkneVh9/YD0pK3IsjWxQUVMDD6drlG7eJpo1rXBtctBqDyBm/k+oKHRAm1x9XWT3JFC82QI9YOXXA==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.7.1.tgz", + "integrity": "sha512-bavdk2BA5r3MYalGKZ01u8PGuDBloQmzpBZVhDLrOOv1N943Wq6dcM9GhB3x8b7AbqPMEezauv4PeGkAJfy7FQ==", "license": "MIT", "dependencies": { - "react-router": "7.7.0" + "react-router": "7.7.1" }, "engines": { "node": ">=20.0.0" @@ -4356,19 +4561,6 @@ "react-dom": ">=18" } }, - "node_modules/react-toastify": { - "version": "11.0.5", - "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-11.0.5.tgz", - "integrity": "sha512-EpqHBGvnSTtHYhCPLxML05NLY2ZX0JURbAdNYa6BUkk+amz4wbKBQvoKQAB0ardvSarUBuY4Q4s1sluAzZwkmA==", - "license": "MIT", - "dependencies": { - "clsx": "^2.1.1" - }, - "peerDependencies": { - "react": "^18 || ^19", - "react-dom": "^18 || ^19" - } - }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -4565,6 +4757,16 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/sonner": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.6.tgz", + "integrity": "sha512-yHFhk8T/DK3YxjFQXIrcHT1rGEeTLliVzWbO0xN8GberVun2RiBnxAjXAYpZrqwEVHBG9asI/Li8TAAhN9m59Q==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -4645,13 +4847,13 @@ } }, "node_modules/synckit": { - "version": "0.11.8", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.8.tgz", - "integrity": "sha512-+XZ+r1XGIJGeQk3VvXhT6xx/VpbHsRzsTkGgF6E5RX9TTXD0118l87puaEBZ566FhqblC6U0d4XnubznJDm30A==", + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz", + "integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==", "dev": true, "license": "MIT", "dependencies": { - "@pkgr/core": "^0.2.4" + "@pkgr/core": "^0.2.9" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -4780,9 +4982,9 @@ } }, "node_modules/tw-animate-css": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.3.5.tgz", - "integrity": "sha512-t3u+0YNoloIhj1mMXs779P6MO9q3p3mvGn4k1n3nJPqJw/glZcuijG2qTSN4z4mgNRfW5ZC3aXJFLwDtiipZXA==", + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.3.6.tgz", + "integrity": "sha512-9dy0R9UsYEGmgf26L8UcHiLmSFTHa9+D7+dAt/G/sF5dCnPePZbfgDYinc7/UzAM7g/baVrmS6m9yEpU46d+LA==", "dev": true, "license": "MIT", "funding": { @@ -4817,16 +5019,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.37.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.37.0.tgz", - "integrity": "sha512-TnbEjzkE9EmcO0Q2zM+GE8NQLItNAJpMmED1BdgoBMYNdqMhzlbqfdSwiRlAzEK2pA9UzVW0gzaaIzXWg2BjfA==", + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.38.0.tgz", + "integrity": "sha512-FsZlrYK6bPDGoLeZRuvx2v6qrM03I0U0SnfCLPs/XCCPCFD80xU9Pg09H/K+XFa68uJuZo7l/Xhs+eDRg2l3hg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.37.0", - "@typescript-eslint/parser": "8.37.0", - "@typescript-eslint/typescript-estree": "8.37.0", - "@typescript-eslint/utils": "8.37.0" + "@typescript-eslint/eslint-plugin": "8.38.0", + "@typescript-eslint/parser": "8.38.0", + "@typescript-eslint/typescript-estree": "8.38.0", + "@typescript-eslint/utils": "8.38.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4889,14 +5091,14 @@ } }, "node_modules/vite": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.0.5.tgz", - "integrity": "sha512-1mncVwJxy2C9ThLwz0+2GKZyEXuC3MyWtAAlNftlZZXZDP3AJt5FmwcMit/IGGaNZ8ZOB2BNO/HFUB+CpN0NQw==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.0.6.tgz", + "integrity": "sha512-MHFiOENNBd+Bd9uvc8GEsIzdkn1JxMmEeYX35tI3fv0sJBUTfW5tQsoaOwuY4KhBI09A3dUJ/DXf2yxPVPUceg==", "license": "MIT", "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.6", - "picomatch": "^4.0.2", + "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.40.0", "tinyglobby": "^0.2.14" diff --git a/frontend/package.json b/frontend/package.json index 86d75aff..f6ce37b5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,6 +14,9 @@ "format:fix": "npm run prettier -- --write" }, "dependencies": { + "@radix-ui/react-dropdown-menu": "^2.1.15", + "@radix-ui/react-progress": "^1.1.7", + "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slot": "^1.2.3", "@tailwindcss/vite": "^4.1.11", "@tanstack/react-query": "^5.83.0", @@ -21,17 +24,20 @@ "axios": "^1.10.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "date-fns": "^4.1.0", "dotenv": "^17.2.0", "lucide-react": "^0.525.0", + "next-themes": "^0.4.6", "react": "^19.1.0", "react-dom": "^19.1.0", "react-router-dom": "^7.7.0", - "react-toastify": "^11.0.5", + "sonner": "^2.0.6", "tailwind-merge": "^3.3.1", "tailwindcss": "^4.1.11" }, "devDependencies": { "@eslint/js": "^9.29.0", + "@types/date-fns": "^2.5.3", "@types/node": "^24.0.15", "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", @@ -45,6 +51,7 @@ "husky": "^8.0.0", "lint-staged": "^16.1.2", "prettier": "^3.6.2", + "prettier-plugin-tailwindcss": "^0.6.14", "tw-animate-css": "^1.3.5", "typescript": "~5.8.3", "typescript-eslint": "^8.34.1", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx deleted file mode 100644 index e1cff623..00000000 --- a/frontend/src/App.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { UserProvider } from './context/AuthProvider'; -import Router from './root/Router'; - -function App() { - return ( - - - - ); -} - -export default App; diff --git a/frontend/src/utils/axios.ts b/frontend/src/api/axios.ts similarity index 51% rename from frontend/src/utils/axios.ts rename to frontend/src/api/axios.ts index b612ca65..9ef6ebc8 100644 --- a/frontend/src/utils/axios.ts +++ b/frontend/src/api/axios.ts @@ -1,12 +1,15 @@ -import axios from 'axios'; +import axios from "axios"; + +import { BACKEND_URL } from "@/shared/constants/endpoints"; +import { getAccessToken } from "@/shared/utils/local-storage"; export const api = axios.create({ - baseURL: 'http://localhost:8080/api/v1' + baseURL: BACKEND_URL }); api.interceptors.request.use( config => { - const token = localStorage.getItem('token'); + const token = getAccessToken(); if (token) { config.headers.Authorization = `Bearer ${token}`; } @@ -14,3 +17,5 @@ api.interceptors.request.use( }, error => Promise.reject(error) ); + + diff --git a/frontend/src/api/queries/Leaderboard.ts b/frontend/src/api/queries/Leaderboard.ts new file mode 100644 index 00000000..e1a47cf5 --- /dev/null +++ b/frontend/src/api/queries/Leaderboard.ts @@ -0,0 +1,38 @@ +import type { LeaderboardUser } from "@/shared/types/types"; +import { api } from "../axios"; +import { BACKEND_URL } from "@/shared/constants/endpoints"; +import type { ApiResponse } from "@/shared/types/api"; +import { CURRENT_USER_RANK_QUERY_KEY, LEADERBOARD_QUERY_KEY } from "@/shared/constants/query-keys"; +import { useQuery } from "@tanstack/react-query"; + +const fetchLeaderboard = async (): Promise> => { + const response = await api.get<{ + message: string; + data: LeaderboardUser[]; + }>(`${BACKEND_URL}/api/v1/leaderboard`); + + return response.data; +} + +export const useLeaderboard = () => { + return useQuery({ + queryKey: [LEADERBOARD_QUERY_KEY], + queryFn: fetchLeaderboard, + }); +} + +const fetchCurrentUserRank = async (): Promise> => { + const response = await api.get<{ + message: string; + data: LeaderboardUser; + }>(`${BACKEND_URL}/api/v1/user/leaderboard`); + + return response.data; +} + +export const useCurrentUserRank = () => { + return useQuery({ + queryKey: [CURRENT_USER_RANK_QUERY_KEY], + queryFn: fetchCurrentUserRank, + }); +} \ No newline at end of file diff --git a/frontend/src/api/queries/Overview.ts b/frontend/src/api/queries/Overview.ts new file mode 100644 index 00000000..78e3bba4 --- /dev/null +++ b/frontend/src/api/queries/Overview.ts @@ -0,0 +1,32 @@ +import type { ApiResponse } from "@/shared/types/api"; +import type { Overview } from "@/shared/types/types"; +import { api } from "../axios"; +import { BACKEND_URL } from "@/shared/constants/endpoints"; +import { useQuery } from "@tanstack/react-query"; +import { OVERVIEW_QUERY_KEY } from "@/shared/constants/query-keys"; + +interface OverviewParams { + year: number; + month: number; +} + +const fetchOverview = async ({ year, month }: OverviewParams): Promise> => { + const response = await api.get<{ + message: string; + data: Overview[]; + }>(`${BACKEND_URL}/api/v1/user/overview`, { + params: { + year, + month + } + }); + + return response.data; +} + +export const useOverview = ({ year, month }: OverviewParams) => { + return useQuery({ + queryKey: [OVERVIEW_QUERY_KEY, year, month], + queryFn: () => fetchOverview({ year, month }), + }); +} \ No newline at end of file diff --git a/frontend/src/api/queries/RecentActivities.ts b/frontend/src/api/queries/RecentActivities.ts new file mode 100644 index 00000000..b39b50db --- /dev/null +++ b/frontend/src/api/queries/RecentActivities.ts @@ -0,0 +1,22 @@ +import type { ApiResponse } from "@/shared/types/api"; +import type { RecentActivity } from "@/shared/types/types"; +import { api } from "../axios"; +import { BACKEND_URL } from "@/shared/constants/endpoints"; +import { useQuery } from "@tanstack/react-query"; +import { RECENT_ACTIVITIES_QUERY_KEY } from "@/shared/constants/query-keys"; + +const fetchRecentActivities = async (): Promise> => { + const response = await api.get<{ + message: string; + data: RecentActivity[]; + }>(`${BACKEND_URL}/api/v1/user/contributions/all`); + + return response.data; +}; + +export const useRecentActivities = () => { + return useQuery({ + queryKey: [RECENT_ACTIVITIES_QUERY_KEY], + queryFn: fetchRecentActivities, + }); +} \ No newline at end of file diff --git a/frontend/src/api/queries/UserBadges.ts b/frontend/src/api/queries/UserBadges.ts new file mode 100644 index 00000000..9f50e857 --- /dev/null +++ b/frontend/src/api/queries/UserBadges.ts @@ -0,0 +1,22 @@ +import { BACKEND_URL } from "@/shared/constants/endpoints"; +import { api } from "../axios"; +import type { ApiResponse } from "@/shared/types/api"; +import type { Badge } from "@/shared/types/types"; +import { USER_BADGES_QUERY_KEY } from "@/shared/constants/query-keys"; +import { useQuery } from "@tanstack/react-query"; + +const fetchUserBadges = async (): Promise> => { + const response = await api.get<{ + message: string; + data: Badge[]; + }>(`${BACKEND_URL}/api/v1/user/badges`); + + return response.data; +} + +export const useUserBadges = () => { + return useQuery({ + queryKey: [USER_BADGES_QUERY_KEY], + queryFn: fetchUserBadges, + }); +} \ No newline at end of file diff --git a/frontend/src/api/queries/UserProfileDetails.ts b/frontend/src/api/queries/UserProfileDetails.ts new file mode 100644 index 00000000..3d4639ed --- /dev/null +++ b/frontend/src/api/queries/UserProfileDetails.ts @@ -0,0 +1,22 @@ +import type { User } from "@/shared/types/types"; +import { api } from "../axios"; +import { useQuery } from "@tanstack/react-query"; +import type { ApiResponse } from "@/shared/types/api"; +import { LOGGED_IN_USER_QUERY_KEY } from "@/shared/constants/query-keys"; +import { BACKEND_URL } from "@/shared/constants/endpoints"; + +const fetchLoggedInUser = async (): Promise> => { + const response = await api.get<{ + message: string; + data: User; + }>(`${BACKEND_URL}/api/v1/auth/user`); + + return response.data; +}; + +export const useLoggedInUser = () => { + return useQuery({ + queryKey: [LOGGED_IN_USER_QUERY_KEY], + queryFn: fetchLoggedInUser, + }); +}; diff --git a/frontend/src/utils/react-query.ts b/frontend/src/api/react-query.ts similarity index 76% rename from frontend/src/utils/react-query.ts rename to frontend/src/api/react-query.ts index a97896ce..29c17f06 100644 --- a/frontend/src/utils/react-query.ts +++ b/frontend/src/api/react-query.ts @@ -1,4 +1,4 @@ -import { QueryClient } from '@tanstack/react-query'; +import { QueryClient } from "@tanstack/react-query"; export const queryClient = new QueryClient({ defaultOptions: { diff --git a/frontend/src/assets/default-profile-pic.svg b/frontend/src/assets/default-profile-pic.svg new file mode 100644 index 00000000..a8d11740 --- /dev/null +++ b/frontend/src/assets/default-profile-pic.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/assets/react.svg b/frontend/src/assets/react.svg deleted file mode 100644 index 6c87de9b..00000000 --- a/frontend/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/src/context/AuthProvider.tsx b/frontend/src/context/AuthProvider.tsx deleted file mode 100644 index da7b91ad..00000000 --- a/frontend/src/context/AuthProvider.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { createContext, useMemo, useState, type ReactNode } from 'react'; - -export type User = { - githubId: string; - githubUsername: string; - avatarUrl: string; -}; - -export interface UserContextInterface { - user: User; - login: (user: User, token: string) => void; - logout: () => void; -} - -const defaultUser: User = { - githubId: '', - githubUsername: '', - avatarUrl: '' -}; - -const defaultState: UserContextInterface = { - user: defaultUser, - login: () => { - throw new Error('login must be used within UserProvider'); - }, - logout: () => { - throw new Error('logout must be used within UserProvider'); - } -}; - -export const UserContext = createContext(defaultState); - -type UserProviderProps = { - children: ReactNode; -}; - -export const UserProvider = ({ children }: UserProviderProps) => { - const [user, setUser] = useState(defaultUser); - - const login = (newUser: User, token: string) => { - setUser(newUser); - localStorage.setItem('token', token); - }; - - const logout = () => { - setUser(defaultUser); - localStorage.removeItem('token'); - }; - - const value = useMemo(() => ({ user, login, logout }), [user]); - - return {children}; -}; diff --git a/frontend/src/features/Login/components/LoginComponent.tsx b/frontend/src/features/Login/components/LoginComponent.tsx new file mode 100644 index 00000000..9084984c --- /dev/null +++ b/frontend/src/features/Login/components/LoginComponent.tsx @@ -0,0 +1,51 @@ +import { Button } from "@/shared/components/ui/button"; +import { + Card, + CardContent, + CardFooter, + CardHeader +} from "@/shared/components/ui/card"; +import { GITHUB_AUTH_URL } from "@/shared/constants/endpoints"; +import Coder from "@/assets/coder.svg"; +import { ACCESS_TOKEN_KEY } from "@/shared/constants/local-storage"; + +const LoginComponent = () => { + const handleGithubLogin = () => { + window.location.href = GITHUB_AUTH_URL || ""; + localStorage.setItem(ACCESS_TOKEN_KEY, 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjIsIklzQWRtaW4iOmZhbHNlLCJleHAiOjE3NTQxMzI3NTh9.VKEboNEvSeVKYnqLuBrvTyvx9IglhYzEyeE57x7Qzto') + }; + + return ( + + + Developer Illustration + + + + +
+
+ + +

+ No idea where to start? Try this{" "} + + Code Triage + +

+
+
+ ); +}; + +export default LoginComponent; diff --git a/frontend/src/features/Login/index.tsx b/frontend/src/features/Login/index.tsx new file mode 100644 index 00000000..4114e41d --- /dev/null +++ b/frontend/src/features/Login/index.tsx @@ -0,0 +1,12 @@ +import AuthLayout from "@/shared/layout/AuthLayout"; +import LoginComponent from "@/features/Login/components/LoginComponent"; + +const Login = () => { + return ( + + + + ); +}; + +export default Login; diff --git a/frontend/src/features/MyContributions/index.tsx b/frontend/src/features/MyContributions/index.tsx new file mode 100644 index 00000000..3a454a7c --- /dev/null +++ b/frontend/src/features/MyContributions/index.tsx @@ -0,0 +1,7 @@ +import UserDashboardLayout from "@/shared/layout/UserDashboardLayout"; + +const MyContributions = () => { + return ; +}; + +export default MyContributions; diff --git a/frontend/src/features/UserDashboard/components/Leaderboard.tsx b/frontend/src/features/UserDashboard/components/Leaderboard.tsx new file mode 100644 index 00000000..b1482e45 --- /dev/null +++ b/frontend/src/features/UserDashboard/components/Leaderboard.tsx @@ -0,0 +1,87 @@ +import { useState, type FC } from "react"; +import clsx from "clsx"; + +import { Button } from "@/shared/components/ui/button"; +import { Card } from "@/shared/components/ui/card"; +import LeaderboardCard from "@/features/UserDashboard/components/LeaderboardCard"; +import { useCurrentUserRank, useLeaderboard } from "@/api/queries/Leaderboard"; +import { TrendingUp } from "lucide-react"; + +interface LeaderboardProps { + className?: string; +} + +const Leaderboard: FC = ({ className }) => { + const [viewAll, setViewAll] = useState(false); + + const handleViewAll = () => { + setViewAll(!viewAll); + }; + + const { data, isLoading } = useLeaderboard(); + const leaderboard = data?.data ?? []; + + const leaderboardData = viewAll ? leaderboard : leaderboard?.slice(0, 10); + + const { data: userData } = useCurrentUserRank(); + const currentUser = userData?.data; + + return ( + +
+

Leader Board

+ +
+ + {isLoading ? ( +
+
+
+ ) : leaderboardData?.length === 0 ? ( +
+ +

+ No leaderboard data +

+
+ ) : ( + <> +
+ {leaderboardData?.map(user => ( + + ))} +
+ {!viewAll && ( +
+ +
+ )} + + )} +
+ ); +}; + +export default Leaderboard; diff --git a/frontend/src/features/UserDashboard/components/LeaderboardCard.tsx b/frontend/src/features/UserDashboard/components/LeaderboardCard.tsx new file mode 100644 index 00000000..f2171616 --- /dev/null +++ b/frontend/src/features/UserDashboard/components/LeaderboardCard.tsx @@ -0,0 +1,46 @@ +import type { FC } from "react"; + +import Coin from "@/shared/components/common/Coin"; +import { Card, CardContent } from "@/shared/components/ui/card"; + +interface LeaderboardCardProps { + rank: number; + username: string; + repositories: number; + balance: number; +} + +const LeaderboardCard: FC = ({ + rank, + username, + repositories, + balance +}) => { + return ( +
+ + +
+ {rank} +
+
+
{username}
+
+ Contributed to{" "} + + {repositories}{" "} + {repositories > 1 ? "Repositories" : "Repository"} + +
+
+
+ + {balance} +
+
+
+
+ ); +}; + +export default LeaderboardCard; \ No newline at end of file diff --git a/frontend/src/features/UserDashboard/components/Overview.tsx b/frontend/src/features/UserDashboard/components/Overview.tsx new file mode 100644 index 00000000..daeb90b1 --- /dev/null +++ b/frontend/src/features/UserDashboard/components/Overview.tsx @@ -0,0 +1,118 @@ +import { useState, type FC } from "react"; +import clsx from "clsx"; +import { Card } from "@/shared/components/ui/card"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger +} from "@/shared/components/ui/dropdown-menu"; +import OverviewCard from "@/features/UserDashboard/components/OverviewCard"; +import { ChevronDown, TrendingUp } from "lucide-react"; +import { format, subMonths } from "date-fns"; +import { useOverview } from "@/api/queries/Overview"; + +interface OverviewProps { + className?: string; +} + +const getLastNMonths = (n: number) => { + return Array.from({ length: n }).map((_, i) => { + const date = subMonths(new Date(), i); + return { + label: format(date, "MMMM yyyy"), + month: date.getMonth() + 1, + year: date.getFullYear() + }; + }); +}; + +const Overview: FC = ({ className }) => { + const monthOptions = getLastNMonths(3); + const [selectedPeriod, setSelectedPeriod] = useState<{ + month: number; + year: number; + }>(monthOptions[0]); + + const { data, isLoading } = useOverview(selectedPeriod); + const overview = data?.data ?? []; + + const overviewData = overview?.filter(data => { + const date = new Date(data.month); + return ( + date.getFullYear() === selectedPeriod.year && + date.getMonth() + 1 === selectedPeriod.month + ); + }); + + return ( + +
+

Overview

+ + + {monthOptions.find( + opt => + opt.month === selectedPeriod.month && + opt.year === selectedPeriod.year + )?.label ?? "Select Month"} + + + + {monthOptions.map(option => ( + + setSelectedPeriod({ month: option.month, year: option.year }) + } + className={clsx( + "cursor-pointer rounded-sm px-3 py-2 text-sm hover:bg-gray-100", + selectedPeriod.month === option.month && + selectedPeriod.year === option.year && + "bg-cc-app-gray-background" + )} + > + {option.label} + + ))} + + +
+ {isLoading ? ( +
+
+
+ ) : overviewData?.length === 0 ? ( +
+ +

+ No overview data +

+

+ No activity found for the selected period. +
+ Try selecting a different month or start contributing! +

+
+ ) : ( +
+ {overviewData?.map(user => ( + + ))} +
+ )} +
+ ); +}; + +export default Overview; diff --git a/frontend/src/features/UserDashboard/components/OverviewCard.tsx b/frontend/src/features/UserDashboard/components/OverviewCard.tsx new file mode 100644 index 00000000..81e19767 --- /dev/null +++ b/frontend/src/features/UserDashboard/components/OverviewCard.tsx @@ -0,0 +1,32 @@ +import { type FC } from "react"; +import Coin from "@/shared/components/common/Coin"; +import { Card } from "@/shared/components/ui/card"; + +interface OverviewCardProps { + type: string; + count: number; + totalCoins: number; +} + +const OverviewCard: FC = ({ type, count, totalCoins }) => { + return ( + +
+

{type}

+
+ + {count} + +
+ + + {totalCoins} + +
+
+
+
+ ); +}; + +export default OverviewCard; diff --git a/frontend/src/features/UserDashboard/components/RecentActivities.tsx b/frontend/src/features/UserDashboard/components/RecentActivities.tsx new file mode 100644 index 00000000..614807a0 --- /dev/null +++ b/frontend/src/features/UserDashboard/components/RecentActivities.tsx @@ -0,0 +1,89 @@ +import { useState, type FC } from "react"; +import clsx from "clsx"; +import { Button } from "@/shared/components/ui/button"; +import { Card } from "@/shared/components/ui/card"; +import ActivityCard from "@/shared/components/common/ActivityCard"; +import { useRecentActivities } from "@/api/queries/RecentActivities"; +import { Link } from "react-router-dom"; +import { TrendingUp } from "lucide-react"; + +interface RecentActivitiesProps { + className?: string; +} + +const RecentActivities: FC = ({ className }) => { + const [viewAll, setViewAll] = useState(false); + + const handleViewAll = () => { + setViewAll(!viewAll); + }; + + const { data, isLoading } = useRecentActivities(); + const recentActivities = data?.data ?? []; + const recentActivitiesData = viewAll + ? recentActivities + : recentActivities?.slice(0, 4); + + return ( + +
+

Recent Activities

+ +
+ + {isLoading ? ( +
+
+
+ ) : recentActivitiesData?.length === 0 ? ( +
+ +

+ No recent activities found +

+
+ ) : ( +
+ {recentActivitiesData?.map((activity, index) => ( + + ))} + {!viewAll && ( +
+ + How does points work? + +
+ )} +
+ )} +
+ ); +}; + +export default RecentActivities; diff --git a/frontend/src/features/UserDashboard/components/UserDashboardComponent.tsx b/frontend/src/features/UserDashboard/components/UserDashboardComponent.tsx new file mode 100644 index 00000000..176d2692 --- /dev/null +++ b/frontend/src/features/UserDashboard/components/UserDashboardComponent.tsx @@ -0,0 +1,17 @@ +import Leaderboard from "@/features/UserDashboard/components/Leaderboard"; +import RecentActivities from "@/features/UserDashboard/components/RecentActivities"; +import Overview from "@/features/UserDashboard/components/Overview"; + +const UserDashboardComponent = () => { + return ( +
+ +
+ + +
+
+ ); +}; + +export default UserDashboardComponent; diff --git a/frontend/src/features/UserDashboard/index.tsx b/frontend/src/features/UserDashboard/index.tsx new file mode 100644 index 00000000..dcc63321 --- /dev/null +++ b/frontend/src/features/UserDashboard/index.tsx @@ -0,0 +1,12 @@ +import UserDashboardLayout from "@/shared/layout/UserDashboardLayout"; +import UserDashboardComponent from "@/features/UserDashboard/components/UserDashboardComponent"; + +const UserDashboard = () => { + return ( + + + + ); +}; + +export default UserDashboard; diff --git a/frontend/src/index.css b/frontend/src/index.css index d0031645..d277bc17 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -56,12 +56,12 @@ --sidebar-border: oklch(0.922 0 0); --sidebar-ring: oklch(0.708 0 0); - --cc-light-blue: oklch(0.6686 0.1358 231.66); - --cc-mid-blue:oklch(0.6163 0.140573 239.7492); - --cc-dark-blue:oklch(0.4668 0.1625 256.62); - --cc-app-blue:oklch(0.3876 0.1761 261.76); - --cc-app-gray-background:oklch(0.9585 0.0195 270.21); - --cc-app-orange:oklch(0.7362 0.1641 62.07); + --cc-app-light-blue: oklch(0.6686 0.1358 231.66); + --cc-app-sky-blue: oklch(0.6163 0.140573 239.7492); + --cc-app-mid-blue: oklch(0.4668 0.1625 256.62); + --cc-app-blue: oklch(0.3876 0.1761 261.76); + --cc-app-gray-background: oklch(0.9585 0.0195 270.21); + --cc-app-orange: oklch(0.7362 0.1641 62.07); } @media (prefers-color-scheme: light) { @@ -101,34 +101,6 @@ --color-chart-5: var(--chart-5); --color-sidebar: var(--sidebar); --color-sidebar-foreground: var(--sidebar-foreground); - --color-sidebar-prima --radius-md: calc(var(--radius) - 2px); - --radius-lg: var(--radius); - --radius-xl: calc(var(--radius) + 4px); - --color-background: var(--background); - --color-foreground: var(--foreground); - --color-card: var(--card); - --color-card-foreground: var(--card-foreground); - --color-popover: var(--popover); - --color-popover-foreground: var(--popover-foreground); - --color-primary: var(--primary); - --color-primary-foreground: var(--primary-foreground); - --color-secondary: var(--secondary); - --color-secondary-foreground: var(--secondary-foreground); - --color-muted: var(--muted); - --color-muted-foreground: var(--muted-foreground); - --color-accent: var(--accent); - --color-accent-foreground: var(--accent-foreground); - --color-destructive: var(--destructive); - --color-border: var(--border); - --color-input: var(--input); - --color-ring: var(--ring); - --color-chart-1: var(--chart-1); - --color-chart-2: var(--chart-2); - --color-chart-3: var(--chart-3); - --color-chart-4: var(--chart-4); - --color-chart-5: var(--chart-5); - --color-sidebar: var(--sidebar); - --color-sidebar-foreground: var(--sidebar-foreground); --color-sidebar-primary: var(--sidebar-primary); --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); --color-sidebar-acry: var(--sidebar-primary); @@ -138,9 +110,9 @@ --color-sidebar-border: var(--sidebar-border); --color-sidebar-ring: var(--sidebar-ring); - --color-cc-light-blue: var(--cc-light-blue); - --color-cc-mid-blue: var(--cc-mid-blue); - --color-cc-dark-blue: var(--cc-dark-blue); + --color-cc-app-light-blue: var(--cc-app-light-blue); + --color-cc-app-sky-blue: var(--cc-app-sky-blue); + --color-cc-app-mid-blue: var(--cc-app-mid-blue); --color-cc-app-blue: var(--cc-app-blue); --color-cc-app-gray-background: var(--cc-app-gray-background); --color-cc-app-orange: var(--cc-app-orange); @@ -184,7 +156,17 @@ * { @apply border-border outline-ring/50; } + body { @apply bg-background text-foreground; } -} \ No newline at end of file + + .no-scrollbar::-webkit-scrollbar { + display: none; + } + + .no-scrollbar { + -ms-overflow-style: none; + scrollbar-width: none; + } +} diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts deleted file mode 100644 index bd0c391d..00000000 --- a/frontend/src/lib/utils.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { clsx, type ClassValue } from "clsx" -import { twMerge } from "tailwind-merge" - -export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)) -} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index cb2764c5..817fad21 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -1,14 +1,20 @@ -import { StrictMode } from 'react'; -import { createRoot } from 'react-dom/client'; -import './index.css'; -import App from './App.tsx'; -import { QueryClientProvider } from '@tanstack/react-query'; -import { queryClient } from './utils/react-query.ts'; +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import { QueryClientProvider } from "@tanstack/react-query"; +import { Toaster } from "sonner"; -createRoot(document.getElementById('root')!).render( +import Router from "@/root/Router"; +import { AuthProvider } from "@/shared/context/AuthProvider"; +import { queryClient } from "@/api/react-query.ts"; +import "./index.css"; + +createRoot(document.getElementById("root")!).render( - + + + + ); diff --git a/frontend/src/pages/Login/index.tsx b/frontend/src/pages/Login/index.tsx deleted file mode 100644 index 7ee300a6..00000000 --- a/frontend/src/pages/Login/index.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import type { FC } from 'react'; -import LoginComponent from './loginComponent'; -import AuthLayout from '@/shared/components/AuthLayout'; - -const Login: FC = () => { - return ( - - - - ); -}; - -export default Login; diff --git a/frontend/src/pages/Login/loginComponent.tsx b/frontend/src/pages/Login/loginComponent.tsx deleted file mode 100644 index d60a061b..00000000 --- a/frontend/src/pages/Login/loginComponent.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { Button } from '@/components/ui/button'; -import { Card, CardContent, CardFooter, CardHeader } from '@/components/ui/card'; -import { type FC } from 'react'; -import Coder from '@/assets/coder.svg'; -import { env } from '@/utils/endpoints'; - -const LoginComponent: FC = () => { - const handleGithubLogin = () => { - window.location.href = env.GithubAuthUrl || ''; - }; - - return ( - - - Developer Illustration - - - - -
-
- - -

- No idea where to start? Try this{' '} - - Code Triage - -

-
-
- ); -}; - -export default LoginComponent; diff --git a/frontend/src/root/PrivateRoutes.tsx b/frontend/src/root/PrivateRoutes.tsx deleted file mode 100644 index feebec93..00000000 --- a/frontend/src/root/PrivateRoutes.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { useEffect, type ReactNode } from 'react'; -import { Navigate } from 'react-router-dom'; -import { toast } from 'react-toastify'; -import 'react-toastify/dist/ReactToastify.css'; - -interface PrivateRoutesProps { - children: ReactNode; -} - -const PrivateRoutes: React.FC = ({ children }) => { - const localStorageToken = localStorage.getItem('token'); - - useEffect(() => { - if (!localStorageToken) { - toast.warning('You need to login to proceed!'); - } - }, [localStorageToken]); - - return localStorageToken ? children : ; -}; - -export default PrivateRoutes; diff --git a/frontend/src/root/Router.tsx b/frontend/src/root/Router.tsx index d74e1983..dc00eced 100644 --- a/frontend/src/root/Router.tsx +++ b/frontend/src/root/Router.tsx @@ -1,13 +1,14 @@ -import { createBrowserRouter, RouterProvider } from 'react-router-dom'; -import PrivateRoutes from './PrivateRoutes'; -import { routesConfig, type RoutesType } from './routesConfig'; +import { RouterProvider, createBrowserRouter } from "react-router-dom"; + +import WithAuth from "@/shared/HOC/WithAuth"; +import { type RoutesType, routesConfig } from "@/root/routes-config"; const generateRoutes = (routes: RoutesType[]) => { return routes.map(({ path, element, isProtected }) => { let wrappedElement = element; if (isProtected) { - wrappedElement = {wrappedElement}; + wrappedElement = {wrappedElement}; } return { path, element: wrappedElement }; diff --git a/frontend/src/root/routeConstants.ts b/frontend/src/root/routeConstants.ts deleted file mode 100644 index 48ed62e8..00000000 --- a/frontend/src/root/routeConstants.ts +++ /dev/null @@ -1,4 +0,0 @@ -export const ROUTES = { - LOGIN: '/login', - LANDING: '/' -}; diff --git a/frontend/src/root/routes-config.tsx b/frontend/src/root/routes-config.tsx new file mode 100644 index 00000000..62633f1f --- /dev/null +++ b/frontend/src/root/routes-config.tsx @@ -0,0 +1,34 @@ +import type { ReactNode } from "react"; + +import Login from "@/features/Login"; +import MyContributions from "@/features/MyContributions"; +import UserDashboard from "@/features/UserDashboard"; +import { + LOGIN_PATH, + MY_CONTRIBUTIONS_PATH, + USER_DASHBOARD_PATH +} from "@/shared/constants/routes"; + +export interface RoutesType { + path: string; + element: ReactNode; + isProtected?: boolean; +} + +export const routesConfig: RoutesType[] = [ + { + path: LOGIN_PATH, + element: , + isProtected: false + }, + { + path: USER_DASHBOARD_PATH, + element: , + isProtected: false + }, + { + path: MY_CONTRIBUTIONS_PATH, + element: , + isProtected: false + } +]; diff --git a/frontend/src/root/routesConfig.tsx b/frontend/src/root/routesConfig.tsx deleted file mode 100644 index 28f92c5f..00000000 --- a/frontend/src/root/routesConfig.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import type { ReactNode } from 'react'; -import Login from '../pages/Login'; -import { ROUTES } from './routeConstants'; - -export interface RoutesType { - path: string; - element: ReactNode; - isProtected: boolean; -} - -export const routesConfig: RoutesType[] = [ - { - path: ROUTES.LOGIN, - element: , - isProtected: false - } - // { - // path: //path, - // element: ( - // - // {/* protected component */} - // - // ), - // }, -]; diff --git a/frontend/src/shared/HOC/WithAuth.tsx b/frontend/src/shared/HOC/WithAuth.tsx new file mode 100644 index 00000000..fb44e44a --- /dev/null +++ b/frontend/src/shared/HOC/WithAuth.tsx @@ -0,0 +1,29 @@ +import { type FC, type ReactNode, useEffect } from "react"; +import { Navigate, useLocation, useNavigate } from "react-router-dom"; + +import { LOGIN_PATH } from "@/shared/constants/routes"; +import { getAccessToken } from "@/shared/utils/local-storage"; + +interface WithAuthProps { + children: ReactNode; +} + +const WithAuth: FC = ({ children }) => { + const navigate = useNavigate(); + const location = useLocation(); + const userAccessToken = getAccessToken(); + + useEffect(() => { + if (!userAccessToken) { + navigate(LOGIN_PATH, { replace: true }); + } + }, [userAccessToken, location.pathname, navigate]); + + if (!userAccessToken) { + return ; + } + + return <>{children}; +}; + +export default WithAuth; diff --git a/frontend/src/shared/components/AuthLayout.tsx b/frontend/src/shared/components/AuthLayout.tsx deleted file mode 100644 index 5dd61434..00000000 --- a/frontend/src/shared/components/AuthLayout.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import { useEffect, type FC, type ReactNode } from 'react'; -import { useLocation, useNavigate } from 'react-router-dom'; -import { getAccessToken, getUserData } from '../utils/local-storage'; -import { ROUTES } from '@/root/routeConstants'; -import { Card } from '@/components/ui/card'; -import { CheckCircle } from 'lucide-react'; - -interface AuthLayoutProps { - children: ReactNode; -} - -const AuthLayout: FC = ({ children }) => { - const location = useLocation(); - const navigate = useNavigate(); - - useEffect(() => { - const userAccessToken = getAccessToken(); - const userData = getUserData(); - const shouldRedirect = [ROUTES.LOGIN].includes(location.pathname); - - if (userAccessToken && userData && shouldRedirect) { - navigate(ROUTES.LANDING); - } - }, [navigate, location.pathname]); - - return ( -
-
- -
-
- {Array.from({ length: 64 }).map((_, i) => ( -
- ))} -
-
- -
-
-
-
-

- Code Curiosity -

-
- -
- {[ - 'Earn and Upskill', - 'Set Your Goals', - 'Leader Board', - 'Open Source Contribution' - ].map((text, i) => ( -
- - - {text} - -
- ))} -
-
-
-
- - {/* Bottom Grid Decoration */} -
-
- {Array.from({ length: 64 }).map((_, i) => ( -
- ))} -
-
- - - - {children} - -
-
- ); -}; - -export default AuthLayout; diff --git a/frontend/src/shared/components/UserDashboard/Navbar.tsx b/frontend/src/shared/components/UserDashboard/Navbar.tsx new file mode 100644 index 00000000..b509d225 --- /dev/null +++ b/frontend/src/shared/components/UserDashboard/Navbar.tsx @@ -0,0 +1,30 @@ +import { Link, useLocation } from "react-router-dom"; + +import { Card } from "@/shared/components/ui/card"; +import { USER_DASHBOARD_NAVBAR_OPTIONS } from "@/shared/types/navbar"; + +const Navbar = () => { + const location = useLocation(); + + const isActive = (path: string) => location.pathname === path; + + return ( + + {USER_DASHBOARD_NAVBAR_OPTIONS.map(option => ( + + {option.name} + + ))} + + ); +}; + +export default Navbar; diff --git a/frontend/src/shared/components/UserDashboard/UserBadges.tsx b/frontend/src/shared/components/UserDashboard/UserBadges.tsx new file mode 100644 index 00000000..0cc35a75 --- /dev/null +++ b/frontend/src/shared/components/UserDashboard/UserBadges.tsx @@ -0,0 +1,50 @@ +import { useUserBadges } from "@/api/queries/UserBadges"; +import { Star } from "lucide-react"; +import type { Badge } from "@/shared/types/types"; + +const badgeColorMap: Record = { + BEGINNER: "text-[#cd7f32]", + INTERMEDIATE: "text-[#c0c0c0]", + ADVANCED: "text-[#ffd700]", + CUSTOM: "text-orange-400", +}; + +const UserBadges = () => { + const { data } = useUserBadges(); + const badges = data?.data ?? []; + + const grouped = badges.reduce>((acc, badge) => { + const type = badge.badgeType.toUpperCase(); + if (!acc[type]) acc[type] = []; + acc[type].push(badge); + return acc; + }, {}); + + return ( +
+

+ BADGES +

+
+ {Object.entries(grouped).map(([type, badgeList]) => { + const color = badgeColorMap[type] ?? "text-gray-400"; + return ( +
+ + {badgeList.length > 1 && ( + Ă—{badgeList.length} + )} +
+ {type}
+
+
+ ); + })} +
+
+ ); +}; + +export default UserBadges; diff --git a/frontend/src/shared/components/UserDashboard/UserGoals.tsx b/frontend/src/shared/components/UserDashboard/UserGoals.tsx new file mode 100644 index 00000000..f6cdd95c --- /dev/null +++ b/frontend/src/shared/components/UserDashboard/UserGoals.tsx @@ -0,0 +1,42 @@ +import { Progress } from "@/shared/components/ui/progress"; +const goals = [ + { name: "Issue Resolve", current: 2, total: 5, progress: 40 }, + { name: "PR Review", current: 6, total: 8, progress: 75 }, + { name: "PR Merge", current: 2, total: 2, progress: 100 }, + { name: "PR Close", current: 1, total: 5, progress: 20 }, + { name: "PR Close", current: 1, total: 5, progress: 20 }, + { name: "PR Close", current: 1, total: 5, progress: 20 }, + { name: "PR Close", current: 1, total: 5, progress: 20 }, + { name: "PR Close", current: 1, total: 5, progress: 20 }, + { name: "PR Close", current: 1, total: 5, progress: 20 }, + { name: "PR Close", current: 1, total: 5, progress: 20 } +]; + +const UserGoals = () => { + return ( +
+

+ MY GOALS (BEGINNER) +

+
+ {goals.map((goal, index) => ( +
+
+ {goal.name} + + {goal.current}/{goal.total} + +
+ +
+ ))} +
+
+ ); +}; + +export default UserGoals; diff --git a/frontend/src/shared/components/UserDashboard/UserProfileCard.tsx b/frontend/src/shared/components/UserDashboard/UserProfileCard.tsx new file mode 100644 index 00000000..36918e54 --- /dev/null +++ b/frontend/src/shared/components/UserDashboard/UserProfileCard.tsx @@ -0,0 +1,28 @@ +import { Card } from "@/shared/components/ui/card"; +import { Separator } from "@/shared/components/ui/separator"; +import UserProfileDetails from "@/shared/components/UserDashboard/UserProfileDetails"; +import UserBadges from "@/shared/components/UserDashboard/UserBadges"; +import UserGoals from "@/shared/components/UserDashboard/UserGoals"; + +const UserProfileCard = () => { + return ( + +
+
+
+ {Array.from({ length: 30 }).map((_, i) => ( +
+ ))} +
+
+ + + + + +
+ + ); +}; + +export default UserProfileCard; diff --git a/frontend/src/shared/components/UserDashboard/UserProfileDetails.tsx b/frontend/src/shared/components/UserDashboard/UserProfileDetails.tsx new file mode 100644 index 00000000..111faf49 --- /dev/null +++ b/frontend/src/shared/components/UserDashboard/UserProfileDetails.tsx @@ -0,0 +1,62 @@ +import { ExternalLink, MoreHorizontal } from "lucide-react"; + +import Coin from "@/shared/components/common/Coin"; +import { Button } from "@/shared/components/ui/button"; +import DefaultProfilePic from "@/assets/default-profile-pic.svg" +import { Separator } from "@/shared/components/ui/separator"; +import { useLoggedInUser } from "@/api/queries/UserProfileDetails"; +import { Link } from "react-router-dom"; + +const UserProfileDetails = () => { + const { data } = useLoggedInUser(); + const user = data?.data + + return ( +
+
+
+
+ Profile +
+
+ +
+

{user?.githubUsername || "username"}

+ + + + +
+
+ +
+
+ + {user?.currentBalance || "0"} +
+
+ + +
+
+
+ ); +}; + +export default UserProfileDetails; diff --git a/frontend/src/shared/components/common/ActivityCard.tsx b/frontend/src/shared/components/common/ActivityCard.tsx new file mode 100644 index 00000000..5d55a755 --- /dev/null +++ b/frontend/src/shared/components/common/ActivityCard.tsx @@ -0,0 +1,58 @@ +import type { FC } from "react"; +import Coin from "@/shared/components/common/Coin"; + +interface ActivityCardProps { + contributionType: string; + repositoryName: string; + contributedAt: string; + balanceChange: number; + showLine: boolean; +} + +const ActivityCard: FC = ({ + contributionType, + repositoryName, + contributedAt, + balanceChange, + showLine = true +}) => { + return ( +
+ {showLine && ( +
+ )} + +
+ +
+
+
+ {contributionType} +
+
+ Contributed to{" "} + + <{repositoryName}> + +
+
+ Contributed on {contributedAt} +
+
+ + {balanceChange && ( +
+ + {balanceChange < 0 ? `-${Math.abs(balanceChange)}` : balanceChange} +
+ )} +
+
+ ); +}; + +export default ActivityCard; diff --git a/frontend/src/shared/components/common/Coin.tsx b/frontend/src/shared/components/common/Coin.tsx new file mode 100644 index 00000000..e85fdbdf --- /dev/null +++ b/frontend/src/shared/components/common/Coin.tsx @@ -0,0 +1,9 @@ +const Coin = () => { + return ( +
+
+
+ ); +}; + +export default Coin; diff --git a/frontend/src/components/ui/button.tsx b/frontend/src/shared/components/ui/button.tsx similarity index 70% rename from frontend/src/components/ui/button.tsx rename to frontend/src/shared/components/ui/button.tsx index a2df8dce..61ab42c9 100644 --- a/frontend/src/components/ui/button.tsx +++ b/frontend/src/shared/components/ui/button.tsx @@ -1,15 +1,15 @@ -import * as React from "react" -import { Slot } from "@radix-ui/react-slot" -import { cva, type VariantProps } from "class-variance-authority" +import * as React from "react"; +import { type VariantProps, cva } from "class-variance-authority"; +import { Slot } from "@radix-ui/react-slot"; -import { cn } from "@/lib/utils" +import { cn } from "@/shared/utils/tailwindcss"; const buttonVariants = cva( "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", { variants: { variant: { - default: + primary: "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", destructive: "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", @@ -20,20 +20,24 @@ const buttonVariants = cva( ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", link: "text-primary underline-offset-4 hover:underline", + ccAppOutlineMidBlue: + "bg-cc-app-mid-blue hover:bg-cc-app-blue rounded-sm border border-white text-white", + ccAppOutline: + "border-cc-app-mid-blue text-cc-app-blue hover:bg-cc-app-mid-blue/5 rounded-sm border focus:outline-none" }, size: { - default: "h-9 px-4 py-2 has-[>svg]:px-3", + md: "h-9 px-4 py-2 has-[>svg]:px-3", sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", lg: "h-10 rounded-md px-6 has-[>svg]:px-4", - icon: "size-9", - }, + icon: "size-9" + } }, defaultVariants: { - variant: "default", - size: "default", - }, + variant: "primary", + size: "md" + } } -) +); function Button({ className, @@ -43,9 +47,9 @@ function Button({ ...props }: React.ComponentProps<"button"> & VariantProps & { - asChild?: boolean + asChild?: boolean; }) { - const Comp = asChild ? Slot : "button" + const Comp = asChild ? Slot : "button"; return ( - ) + ); } -export { Button, buttonVariants } +export { Button, buttonVariants }; diff --git a/frontend/src/components/ui/card.tsx b/frontend/src/shared/components/ui/card.tsx similarity index 93% rename from frontend/src/components/ui/card.tsx rename to frontend/src/shared/components/ui/card.tsx index d05bbc6c..f27e1678 100644 --- a/frontend/src/components/ui/card.tsx +++ b/frontend/src/shared/components/ui/card.tsx @@ -1,6 +1,6 @@ -import * as React from "react" +import * as React from "react"; -import { cn } from "@/lib/utils" +import { cn } from "@/shared/utils/tailwindcss"; function Card({ className, ...props }: React.ComponentProps<"div">) { return ( @@ -12,7 +12,7 @@ function Card({ className, ...props }: React.ComponentProps<"div">) { )} {...props} /> - ) + ); } function CardHeader({ className, ...props }: React.ComponentProps<"div">) { @@ -25,7 +25,7 @@ function CardHeader({ className, ...props }: React.ComponentProps<"div">) { )} {...props} /> - ) + ); } function CardTitle({ className, ...props }: React.ComponentProps<"div">) { @@ -35,7 +35,7 @@ function CardTitle({ className, ...props }: React.ComponentProps<"div">) { className={cn("leading-none font-semibold", className)} {...props} /> - ) + ); } function CardDescription({ className, ...props }: React.ComponentProps<"div">) { @@ -45,7 +45,7 @@ function CardDescription({ className, ...props }: React.ComponentProps<"div">) { className={cn("text-muted-foreground text-sm", className)} {...props} /> - ) + ); } function CardAction({ className, ...props }: React.ComponentProps<"div">) { @@ -58,7 +58,7 @@ function CardAction({ className, ...props }: React.ComponentProps<"div">) { )} {...props} /> - ) + ); } function CardContent({ className, ...props }: React.ComponentProps<"div">) { @@ -68,7 +68,7 @@ function CardContent({ className, ...props }: React.ComponentProps<"div">) { className={cn("px-6", className)} {...props} /> - ) + ); } function CardFooter({ className, ...props }: React.ComponentProps<"div">) { @@ -78,15 +78,15 @@ function CardFooter({ className, ...props }: React.ComponentProps<"div">) { className={cn("flex items-center px-6 [.border-t]:pt-6", className)} {...props} /> - ) + ); } export { Card, - CardHeader, - CardFooter, - CardTitle, CardAction, - CardDescription, CardContent, -} + CardDescription, + CardFooter, + CardHeader, + CardTitle +}; diff --git a/frontend/src/shared/components/ui/dropdown-menu.tsx b/frontend/src/shared/components/ui/dropdown-menu.tsx new file mode 100644 index 00000000..05a5157c --- /dev/null +++ b/frontend/src/shared/components/ui/dropdown-menu.tsx @@ -0,0 +1,255 @@ +import * as React from "react" +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" +import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react" + +import { cn } from "@/shared/utils/tailwindcss" + +function DropdownMenu({ + ...props +}: React.ComponentProps) { + return +} + +function DropdownMenuPortal({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuContent({ + className, + sideOffset = 4, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +function DropdownMenuGroup({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuItem({ + className, + inset, + variant = "default", + ...props +}: React.ComponentProps & { + inset?: boolean + variant?: "default" | "destructive" +}) { + return ( + + ) +} + +function DropdownMenuCheckboxItem({ + className, + children, + checked, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function DropdownMenuRadioGroup({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuRadioItem({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function DropdownMenuLabel({ + className, + inset, + ...props +}: React.ComponentProps & { + inset?: boolean +}) { + return ( + + ) +} + +function DropdownMenuSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuShortcut({ + className, + ...props +}: React.ComponentProps<"span">) { + return ( + + ) +} + +function DropdownMenuSub({ + ...props +}: React.ComponentProps) { + return +} + +function DropdownMenuSubTrigger({ + className, + inset, + children, + ...props +}: React.ComponentProps & { + inset?: boolean +}) { + return ( + + {children} + + + ) +} + +function DropdownMenuSubContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + DropdownMenu, + DropdownMenuPortal, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuLabel, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuSub, + DropdownMenuSubTrigger, + DropdownMenuSubContent, +} diff --git a/frontend/src/shared/components/ui/progress.tsx b/frontend/src/shared/components/ui/progress.tsx new file mode 100644 index 00000000..d2110be8 --- /dev/null +++ b/frontend/src/shared/components/ui/progress.tsx @@ -0,0 +1,35 @@ +import * as React from "react"; +import * as ProgressPrimitive from "@radix-ui/react-progress"; + +import { cn } from "@/shared/utils/tailwindcss"; + +function Progress({ + className, + indicatorClassName, + value, + ...props +}: React.ComponentProps & { + indicatorClassName?: string; +}) { + return ( + + + + ); +} + +export { Progress }; diff --git a/frontend/src/shared/components/ui/separator.tsx b/frontend/src/shared/components/ui/separator.tsx new file mode 100644 index 00000000..70af3ab9 --- /dev/null +++ b/frontend/src/shared/components/ui/separator.tsx @@ -0,0 +1,26 @@ +import * as React from "react"; +import * as SeparatorPrimitive from "@radix-ui/react-separator"; + +import { cn } from "@/shared/utils/tailwindcss"; + +function Separator({ + className, + orientation = "horizontal", + decorative = true, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { Separator }; diff --git a/frontend/src/shared/components/ui/sonner.tsx b/frontend/src/shared/components/ui/sonner.tsx new file mode 100644 index 00000000..85514eca --- /dev/null +++ b/frontend/src/shared/components/ui/sonner.tsx @@ -0,0 +1,23 @@ +import { useTheme } from "next-themes"; +import { Toaster as Sonner, type ToasterProps } from "sonner"; + +const Toaster = ({ ...props }: ToasterProps) => { + const { theme = "system" } = useTheme(); + + return ( + + ); +}; + +export { Toaster }; diff --git a/frontend/src/shared/constants/endpoints.ts b/frontend/src/shared/constants/endpoints.ts new file mode 100644 index 00000000..ac10321c --- /dev/null +++ b/frontend/src/shared/constants/endpoints.ts @@ -0,0 +1,3 @@ +export const GITHUB_AUTH_URL = import.meta.env.VITE_GITHUB_AUTH_URL as string; +export const BACKEND_URL = import.meta.env.VITE_BACKEND_URL as string; +export const FRONTEND_URL = import.meta.env.VITE_FRONTEND_URL as string; diff --git a/frontend/src/shared/constants/local-storage.ts b/frontend/src/shared/constants/local-storage.ts new file mode 100644 index 00000000..964b3b29 --- /dev/null +++ b/frontend/src/shared/constants/local-storage.ts @@ -0,0 +1 @@ +export const ACCESS_TOKEN_KEY = "cc-7db23e66-accessToken"; diff --git a/frontend/src/shared/constants/query-keys.ts b/frontend/src/shared/constants/query-keys.ts new file mode 100644 index 00000000..5f5b57c5 --- /dev/null +++ b/frontend/src/shared/constants/query-keys.ts @@ -0,0 +1,6 @@ +export const LOGGED_IN_USER_QUERY_KEY = "logged-in-user" +export const USER_BADGES_QUERY_KEY = "user-badges" +export const LEADERBOARD_QUERY_KEY="leaderboard" +export const CURRENT_USER_RANK_QUERY_KEY="current-user-rank" +export const RECENT_ACTIVITIES_QUERY_KEY="recent-activities" +export const OVERVIEW_QUERY_KEY="overview" \ No newline at end of file diff --git a/frontend/src/shared/constants/routes.ts b/frontend/src/shared/constants/routes.ts new file mode 100644 index 00000000..8cbdabfc --- /dev/null +++ b/frontend/src/shared/constants/routes.ts @@ -0,0 +1,4 @@ +export const LOGIN_PATH = "/login"; + +export const USER_DASHBOARD_PATH = "/"; +export const MY_CONTRIBUTIONS_PATH = "/my-contributions"; diff --git a/frontend/src/shared/context/AuthProvider.tsx b/frontend/src/shared/context/AuthProvider.tsx new file mode 100644 index 00000000..a5e1340f --- /dev/null +++ b/frontend/src/shared/context/AuthProvider.tsx @@ -0,0 +1,53 @@ +import { type ReactNode, createContext, useMemo, useState } from "react"; + +import { clearAccessToken, setAccessToken } from "@/shared/utils/local-storage"; + +export type UserCredentials = { + githubId: string; + githubUsername: string; + avatarUrl: string; +}; + +export interface AuthContextInterface { + userCredentials: UserCredentials | null; + login: (userCredentials: UserCredentials, token: string) => void; + logout: () => void; +} + +const AuthContext = createContext({ + userCredentials: null, + login: () => { + throw new Error("AuthContext: login called outside AuthProvider"); + }, + logout: () => { + throw new Error("AuthContext: logout called outside AuthProvider"); + } +}); + +type AuthProviderProps = { + children: ReactNode; +}; + +export const AuthProvider = ({ children }: AuthProviderProps) => { + const [userCredentials, setUserCredentials] = + useState(null); + + const login = (userCredentials: UserCredentials, token: string) => { + setUserCredentials(userCredentials); + setAccessToken(token); + }; + + const logout = () => { + setUserCredentials(null); + clearAccessToken(); + }; + + const value = useMemo( + () => ({ userCredentials, login, logout }), + [userCredentials] + ); + + return {children}; +}; + +export { AuthContext }; diff --git a/frontend/src/shared/layout/AuthLayout.tsx b/frontend/src/shared/layout/AuthLayout.tsx new file mode 100644 index 00000000..9cbad0b2 --- /dev/null +++ b/frontend/src/shared/layout/AuthLayout.tsx @@ -0,0 +1,86 @@ +import { type FC, type ReactNode, useEffect } from "react"; +import { useLocation, useNavigate } from "react-router-dom"; +import { CheckCircle } from "lucide-react"; + +import { Card } from "@/shared/components/ui/card"; +import { LOGIN_PATH, USER_DASHBOARD_PATH } from "@/shared/constants/routes"; +import { getAccessToken } from "@/shared/utils/local-storage"; + +interface AuthLayoutProps { + children: ReactNode; +} + +const AuthLayout: FC = ({ children }) => { + const location = useLocation(); + const navigate = useNavigate(); + + useEffect(() => { + const userAccessToken = getAccessToken(); + const shouldRedirect = [LOGIN_PATH].includes(location.pathname); + + if (userAccessToken && shouldRedirect) { + navigate(USER_DASHBOARD_PATH); + } + }, [navigate, location]); + + return ( +
+
+ +
+
+ {Array.from({ length: 64 }).map((_, i) => ( +
+ ))} +
+
+ +
+
+
+
+

+ Code Curiosity +

+
+ +
+ {[ + "Earn and Upskill", + "Set Your Goals", + "Leader Board", + "Open Source Contribution" + ].map((text, i) => ( +
+ + + {text} + +
+ ))} +
+
+
+
+ +
+
+ {Array.from({ length: 64 }).map((_, i) => ( +
+ ))} +
+
+ + + + {children} + +
+
+ ); +}; + +export default AuthLayout; diff --git a/frontend/src/shared/layout/UserDashboardLayout.tsx b/frontend/src/shared/layout/UserDashboardLayout.tsx new file mode 100644 index 00000000..fd087c22 --- /dev/null +++ b/frontend/src/shared/layout/UserDashboardLayout.tsx @@ -0,0 +1,24 @@ +import type { FC, ReactNode } from "react"; + +import Navbar from "@/shared/components/UserDashboard/Navbar"; +import UserProfileCard from "@/shared/components/UserDashboard/UserProfileCard"; + +interface UserDashboardLayoutProps { + children?: ReactNode; +} + +const UserDashboardLayout: FC = ({ children }) => { + return ( +
+ +
+ +
+ {children} +
+
+
+ ); +}; + +export default UserDashboardLayout; diff --git a/frontend/src/shared/lib/constants.ts b/frontend/src/shared/lib/constants.ts deleted file mode 100644 index 7ed62cd7..00000000 --- a/frontend/src/shared/lib/constants.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const ACCESS_TOKEN_KEY = 'accessToken'; -export const USER_DATA_KEY = 'userData'; diff --git a/frontend/src/shared/types/api.ts b/frontend/src/shared/types/api.ts new file mode 100644 index 00000000..c4e703da --- /dev/null +++ b/frontend/src/shared/types/api.ts @@ -0,0 +1,4 @@ +export interface ApiResponse { + message :string; + data: T; +} \ No newline at end of file diff --git a/frontend/src/shared/types/navbar.ts b/frontend/src/shared/types/navbar.ts new file mode 100644 index 00000000..157b6784 --- /dev/null +++ b/frontend/src/shared/types/navbar.ts @@ -0,0 +1,6 @@ +import { MY_CONTRIBUTIONS_PATH, USER_DASHBOARD_PATH } from "@/shared/constants/routes"; + +export const USER_DASHBOARD_NAVBAR_OPTIONS = [ + { name: "Dashboard", path: USER_DASHBOARD_PATH }, + { name: "My Contributions", path: MY_CONTRIBUTIONS_PATH } +]; diff --git a/frontend/src/shared/types/types.ts b/frontend/src/shared/types/types.ts new file mode 100644 index 00000000..d53b24f2 --- /dev/null +++ b/frontend/src/shared/types/types.ts @@ -0,0 +1,58 @@ +export interface User { + userId: number; + githubId: number; + githubUsername: string; + email: string; + avatarUrl: string; + currentBalance: number; + currentActiveGoalId: number; + isBlocked: boolean; + isAdmin: boolean; + password: string; + isDeleted: boolean; + deletedAt: string; + createdAt: string; + updatedAt: string; +} + +export interface Badge { + id: number; + userId: number; + badgeType: string; + earnedAt: string; + createdAt: string; +} + +export interface LeaderboardUser { + id: number; + githubUsername: string; + avatarUrl: string; + contributedReposCount: number; + currentBalance: number; + rank: number; +} + +export interface RecentActivity { + userId: number; + repositoryId: number; + contributionScoreId: number; + contributionType: string; + balanceChange: number; + contributedAt: string; + githubEventId: number; + githubRepoId: number; + repoName: string; + description: string; + languagesUrl: string; + repoUrl: string; + ownerName: string; + updateDate: string; + contributorsUrl: string; +} + +export interface Overview { + type: string; + count: number; + totalCoins: number; + month: string; +} diff --git a/frontend/src/shared/utils/local-storage.ts b/frontend/src/shared/utils/local-storage.ts index 4c7be901..455701cd 100644 --- a/frontend/src/shared/utils/local-storage.ts +++ b/frontend/src/shared/utils/local-storage.ts @@ -1,19 +1,4 @@ -import { USER_DATA_KEY, ACCESS_TOKEN_KEY } from '@/shared/lib/constants'; -import type { User } from '../types/auth'; - -export const getUserData = () => { - try { - const userData = localStorage.getItem(USER_DATA_KEY); - return userData ? (JSON.parse(userData) as User) : null; - // eslint-disable-next-line @typescript-eslint/no-unused-vars - } catch (error) { - return null; - } -}; - -export const setUserData = (data: User) => { - localStorage.setItem(USER_DATA_KEY, JSON.stringify(data)); -}; +import { ACCESS_TOKEN_KEY } from "@/shared/constants/local-storage"; export const getAccessToken = (): string | null => { return localStorage.getItem(ACCESS_TOKEN_KEY) || null; @@ -23,7 +8,6 @@ export const setAccessToken = (token: string) => { localStorage.setItem(ACCESS_TOKEN_KEY, token); }; -export const clearUserCredentials = () => { - localStorage.removeItem(USER_DATA_KEY); +export const clearAccessToken = () => { localStorage.removeItem(ACCESS_TOKEN_KEY); }; diff --git a/frontend/src/shared/utils/tailwindcss.ts b/frontend/src/shared/utils/tailwindcss.ts new file mode 100644 index 00000000..365058ce --- /dev/null +++ b/frontend/src/shared/utils/tailwindcss.ts @@ -0,0 +1,6 @@ +import { type ClassValue, clsx } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} diff --git a/frontend/src/utils/endpoints.ts b/frontend/src/utils/endpoints.ts deleted file mode 100644 index 6708f2c8..00000000 --- a/frontend/src/utils/endpoints.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const env = { - GithubAuthUrl: import.meta.env.VITE_GITHUB_AUTH_URL as string, - BackendUrl: import.meta.env.VITE_BACKEND_URL as string, - ServerBaseUrl: import.meta.env.VITE_SERVER_BASE_URL as string -}; diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index e916a26c..0e4ba5f0 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -1,60 +1,60 @@ /** @type {import('tailwindcss').Config} */ module.exports = { - content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], + content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], theme: { extend: { borderRadius: { - lg: 'var(--radius)', - md: 'calc(var(--radius) - 2px)', - sm: 'calc(var(--radius) - 4px)' + lg: "var(--radius)", + md: "calc(var(--radius) - 2px)", + sm: "calc(var(--radius) - 4px)" }, colors: { - cclightblue: 'hsl(var(--cc-light-blue))', - ccmidblue: 'hsl(var(--cc-mid-blue))', - ccdarkblue: 'hsl(var(--cc-dark-blue))', - ccappblue: 'hsl(var(--cc-app-blue))', - ccappgraybackground: 'hsl(var(--cc-app-gray-background))', - ccapporange: 'hsl(var(--cc-app-orange))', + cclightblue: "hsl(var(--cc-app-light-blue))", + ccskyblue: "hsl(var(--cc-app-sky-blue))", + ccmidblue: "hsl(var(--cc-app-mid-blue))", + ccappblue: "hsl(var(--cc-app-blue))", + ccappgraybackground: "hsl(var(--cc-app-gray-background))", + ccapporange: "hsl(var(--cc-app-orange))", - background: 'hsl(var(--background))', - foreground: 'hsl(var(--foreground))', + background: "hsl(var(--background))", + foreground: "hsl(var(--foreground))", card: { - DEFAULT: 'hsl(var(--card))', - foreground: 'hsl(var(--card-foreground))' + DEFAULT: "hsl(var(--card))", + foreground: "hsl(var(--card-foreground))" }, popover: { - DEFAULT: 'hsl(var(--popover))', - foreground: 'hsl(var(--popover-foreground))' + DEFAULT: "hsl(var(--popover))", + foreground: "hsl(var(--popover-foreground))" }, primary: { - DEFAULT: 'hsl(var(--primary))', - foreground: 'hsl(var(--primary-foreground))' + DEFAULT: "hsl(var(--primary))", + foreground: "hsl(var(--primary-foreground))" }, secondary: { - DEFAULT: 'hsl(var(--secondary))', - foreground: 'hsl(var(--secondary-foreground))' + DEFAULT: "hsl(var(--secondary))", + foreground: "hsl(var(--secondary-foreground))" }, muted: { - DEFAULT: 'hsl(var(--muted))', - foreground: 'hsl(var(--muted-foreground))' + DEFAULT: "hsl(var(--muted))", + foreground: "hsl(var(--muted-foreground))" }, accent: { - DEFAULT: 'hsl(var(--accent))', - foreground: 'hsl(var(--accent-foreground))' + DEFAULT: "hsl(var(--accent))", + foreground: "hsl(var(--accent-foreground))" }, destructive: { - DEFAULT: 'hsl(var(--destructive))', - foreground: 'hsl(var(--destructive-foreground))' + DEFAULT: "hsl(var(--destructive))", + foreground: "hsl(var(--destructive-foreground))" }, - border: 'hsl(var(--border))', - input: 'hsl(var(--input))', - ring: 'hsl(var(--ring))', + border: "hsl(var(--border))", + input: "hsl(var(--input))", + ring: "hsl(var(--ring))", chart: { - 1: 'hsl(var(--chart-1))', - 2: 'hsl(var(--chart-2))', - 3: 'hsl(var(--chart-3))', - 4: 'hsl(var(--chart-4))', - 5: 'hsl(var(--chart-5))' + 1: "hsl(var(--chart-1))", + 2: "hsl(var(--chart-2))", + 3: "hsl(var(--chart-3))", + 4: "hsl(var(--chart-4))", + 5: "hsl(var(--chart-5))" } } } diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json index 0b235e3e..f39faca8 100644 --- a/frontend/tsconfig.app.json +++ b/frontend/tsconfig.app.json @@ -8,9 +8,7 @@ "skipLibCheck": true, "baseUrl": ".", "paths": { - "@/*": [ - "./src/*" - ] + "@/*": ["./src/*"] }, /* Bundler mode */ diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 0747f053..1e173931 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -8,7 +8,7 @@ "path": "./tsconfig.node.json" } ], - "compilerOptions": { + "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["./src/*"] diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index e0e1b904..f916da50 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -1,13 +1,13 @@ -import { defineConfig } from 'vite'; -import react from '@vitejs/plugin-react'; -import tailwindcss from '@tailwindcss/vite'; -import path from "path" +import tailwindcss from "@tailwindcss/vite"; +import react from "@vitejs/plugin-react"; +import path from "path"; +import { defineConfig } from "vite"; export default defineConfig({ plugins: [react(), tailwindcss()], resolve: { alias: { - "@": path.resolve(__dirname, "./src"), - }, - }, + "@": path.resolve(__dirname, "./src") + } + } }); From a1a8a0c8da70cad10680ba6a75edafab35ce8bf5 Mon Sep 17 00:00:00 2001 From: VishakhaSainani-Josh Date: Sun, 27 Jul 2025 00:35:14 +0530 Subject: [PATCH 07/36] refactor code --- backend/README.md | 53 - backend/internal/app/auth/domain.go | 29 +- backend/internal/app/badge/domain.go | 12 +- backend/internal/app/contribution/domain.go | 103 +- backend/internal/app/contribution/service.go | 15 +- backend/internal/app/dependencies.go | 6 +- backend/internal/app/github/domain.go | 12 +- backend/internal/app/goal/domain.go | 26 +- backend/internal/app/repository/domain.go | 70 +- backend/internal/app/repository/service.go | 11 + backend/internal/app/transaction/domain.go | 38 +- backend/internal/app/user/domain.go | 61 +- backend/internal/app/user/service.go | 42 +- .../db/migrations/1748862201_init.up.sql | 2 +- ..._create-index-users-current-balance.up.sql | 4 +- backend/internal/pkg/apperrors/errors.go | 11 +- backend/internal/repository/repository.go | 16 + backend/internal/repository/user.go | 2 + backend/run-backend.sh | 2 +- backend/run-migrations-up.sh | 3 + frontend/yarn.lock | 2047 ----------------- 21 files changed, 271 insertions(+), 2294 deletions(-) delete mode 100644 backend/README.md mode change 100644 => 100755 backend/run-backend.sh create mode 100755 backend/run-migrations-up.sh delete mode 100644 frontend/yarn.lock diff --git a/backend/README.md b/backend/README.md deleted file mode 100644 index 3b2eabfc..00000000 --- a/backend/README.md +++ /dev/null @@ -1,53 +0,0 @@ -What can be measured, can be improved! - -Everyone knows that running is a great exercise. However, by using a wearable like the Fitbit™ to measure your activity, it has motivated people to run regularly. - -CodeCuriosity aims to be the Fitbit™ for open-source contributions -Code Curiosity is a platform to encourage open source contribution, build a cohesive and collaborative open source community and reward open source contributors across the world. It measures open-source contributions (code commits, issues and comments) and gives points that can be redeemed against Github or Amazon Gift Cards! - -CodeCuriosity aims to reward everyone (experts or newbies) and everything (minor changes to features). - -Out of the 30M+ open-source developers on Github, if we can motivate even a fraction to contribute more, we are sure we can make the open-soure community more vibrant and rewarding! We reward open-source contributions from our own pocket, so "The more money we lose, the more the open-source community gains." - -Are you or your company interested in supporting CodeCuriosity? -If you or your company wants to support this cause, please contact us at info@codecuriosity.org. - -You do not have to donate any money! We would expect you to pledge an amount in USD and fulfill redemption requests from registered CodeCuriosity users. Here is the list of companies that are supporting this cause: - -Josh Software has pledged 1000 USD. -You or your company? -What is CodeCuriosity all about? -Here are some questions that should help you get going. A lot more questions that are answered in the FAQ. - -Who should use CodeCuriosity? -Everyone who contributes to open source. It's not only fun but you're also making a difference! Since all commits are rewarded and everyone (experts and newbies alike) is rewarded, it's always encouraging. There are so many repository owners that are looking out for help from developers and CodeCuriosity not only helps you help others but also rewards you while doing it. It's about having your cake and eating it too!. - -How does it work? -We gamify open source contributions. - -Sign-up with your GitHub account on CodeCuriosity. After that, we do the rest! -The system analyses your commits and activities and they are automatically scored and shown on your dashboard. -Choose your monthly goal and try to achieve it. -Every month-end, the cumulative scores are added to your wallet and if you have achieved your goal, you get bonus points! -What are points and how do they work? -Points are the scores you have accumulated for your contributions. You can redeem these points for Github or Amazon Gift cards. We also support other stores and if you send us details, we shall ensure you get rewarded! - -New to Open Source and don't know where to start? -Don't worry! Start with Code Triage. It's a very good way to choose which language and repositories you want to contribute to. It sends you some GitHub issues that you can look at everyday! - -Still too complex for you? Start by reading and contributing back some documentation at DocsDoctor. - -Contributing to CodeCuriosity -If you have some cool ideas, comments or feedback - you can mail us at info@codecuriosity.org. - -We don't have a Slack / IRC channel or mailing list yet -- we shall work on it soon. - -To contribute to code, fork this repository, raise issues, suggest features and help us make the change! - -To test this locally, - -create .env. file based on your environment i.e .env.development, .env.production. For local development, use .env.local -Add github app id and secret to .env. file. Refer env.sample -About -Open-source is now fun and rewarding! - diff --git a/backend/internal/app/auth/domain.go b/backend/internal/app/auth/domain.go index 00f61f9a..33f8f994 100644 --- a/backend/internal/app/auth/domain.go +++ b/backend/internal/app/auth/domain.go @@ -15,26 +15,27 @@ const ( ) type User struct { - Id int `json:"user_id"` - GithubId int `json:"github_id"` - GithubUsername string `json:"github_username"` + Id int `json:"userId"` + GithubId int `json:"githubId"` + GithubUsername string `json:"githubUsername"` Email string `json:"email"` - AvatarUrl string `json:"avatar_url"` - CurrentBalance int `json:"current_balance"` - CurrentActiveGoalId sql.NullInt64 `json:"current_active_goal_id"` - IsBlocked bool `json:"is_blocked"` - IsAdmin bool `json:"is_admin"` + AvatarUrl string `json:"avatarUrl"` + CurrentBalance int `json:"currentBalance"` + CurrentActiveGoalId sql.NullInt64 `json:"currentActiveGoalId"` + IsBlocked bool `json:"isBlocked"` + IsAdmin bool `json:"isAdmin"` Password string `json:"password"` - IsDeleted bool `json:"is_deleted"` - DeletedAt sql.NullTime `json:"deleted_at"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + IsDeleted bool `json:"isDeleted"` + DeletedAt sql.NullTime `json:"deletedAt"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` } type GithubUserResponse struct { GithubId int `json:"id"` GithubUsername string `json:"login"` - AvatarUrl string `json:"avatar_url"` + AvatarUrl string `json:"avatarUrl"` Email string `json:"email"` - IsAdmin bool `json:"is_admin"` + IsAdmin bool `json:"isAdmin"` } + diff --git a/backend/internal/app/badge/domain.go b/backend/internal/app/badge/domain.go index d2c97977..3811923a 100644 --- a/backend/internal/app/badge/domain.go +++ b/backend/internal/app/badge/domain.go @@ -3,10 +3,10 @@ package badge import "time" type Badge struct { - Id int - UserId int - BadgeType string - EarnedAt time.Time - CreatedAt time.Time - UpdatedAt time.Time + Id int `json:"id"` + UserId int `json:"userId"` + BadgeType string `json:"badgeType"` + EarnedAt time.Time `json:"earnedAt"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` } diff --git a/backend/internal/app/contribution/domain.go b/backend/internal/app/contribution/domain.go index 238f5dca..daaea56b 100644 --- a/backend/internal/app/contribution/domain.go +++ b/backend/internal/app/contribution/domain.go @@ -3,68 +3,73 @@ package contribution import "time" type ContributionResponse struct { - ID string `bigquery:"id"` - Type string `bigquery:"type"` - ActorID int `bigquery:"actor_id"` - ActorLogin string `bigquery:"actor_login"` - RepoID int `bigquery:"repo_id"` - RepoName string `bigquery:"repo_name"` - RepoUrl string `bigquery:"repo_url"` - Payload string `bigquery:"payload"` - CreatedAt time.Time `bigquery:"created_at"` + ID string `bigquery:"id" json:"id"` + Type string `bigquery:"type" json:"type"` + ActorID int `bigquery:"actor_id" json:"actorId"` + ActorLogin string `bigquery:"actor_login" json:"actorLogin"` + RepoID int `bigquery:"repo_id" json:"repoId"` + RepoName string `bigquery:"repo_name" json:"repoName"` + RepoUrl string `bigquery:"repo_url" json:"repoUrl"` + Payload string `bigquery:"payload" json:"payload"` + CreatedAt time.Time `bigquery:"created_at" json:"createdAt"` } type Repository struct { - Id int - GithubRepoId int - RepoName string - Description string - LanguagesUrl string - RepoUrl string - OwnerName string - UpdateDate time.Time - ContributorsUrl string - CreatedAt time.Time - UpdatedAt time.Time + Id int `json:"id"` + GithubRepoId int `json:"githubRepoId"` + RepoName string `json:"repoName"` + Description string `json:"description"` + LanguagesUrl string `json:"languagesUrl"` + RepoUrl string `json:"repoUrl"` + OwnerName string `json:"ownerName"` + UpdateDate time.Time `json:"updateDate"` + ContributorsUrl string `json:"contributorsUrl"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` } type Contribution struct { - Id int - UserId int - RepositoryId int - ContributionScoreId int - ContributionType string - BalanceChange int - ContributedAt time.Time - GithubEventId string - CreatedAt time.Time - UpdatedAt time.Time + Id int `json:"id"` + UserId int `json:"userId"` + RepositoryId int `json:"repositoryId"` + ContributionScoreId int `json:"contributionScoreId"` + ContributionType string `json:"contributionType"` + BalanceChange int `json:"balanceChange"` + ContributedAt time.Time `json:"contributedAt"` + GithubEventId string `json:"githubEventId"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` } type ContributionScore struct { - Id int - AdminId int - ContributionType string - Score int - CreatedAt time.Time - UpdatedAt time.Time + Id int `json:"id"` + AdminId int `json:"adminId"` + ContributionType string `json:"contributionType"` + Score int `json:"score"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` } type Transaction struct { - Id int - UserId int - ContributionId int - IsRedeemed bool - IsGained bool - TransactedBalance int - TransactedAt time.Time - CreatedAt time.Time - UpdatedAt time.Time + Id int `json:"id"` + UserId int `json:"userId"` + ContributionId int `json:"contributionId"` + IsRedeemed bool `json:"isRedeemed"` + IsGained bool `json:"isGained"` + TransactedBalance int `json:"transactedBalance"` + TransactedAt time.Time `json:"transactedAt"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` } type MonthlyContributionSummary struct { - Type string - Count int - TotalCoins int - Month time.Time + Type string `json:"type"` + Count int `json:"count"` + TotalCoins int `json:"totalCoins"` + Month time.Time `json:"month"` +} + +type FetchUserContributionsResponse struct { + Contribution + Repository } diff --git a/backend/internal/app/contribution/service.go b/backend/internal/app/contribution/service.go index 610918ae..6a46b14c 100644 --- a/backend/internal/app/contribution/service.go +++ b/backend/internal/app/contribution/service.go @@ -64,7 +64,7 @@ type Service interface { CreateContribution(ctx context.Context, contributionType string, contributionDetails ContributionResponse, repositoryId int, userId int) (Contribution, error) HandleContributionCreation(ctx context.Context, repositoryID int, contribution ContributionResponse) (Contribution, error) GetContributionScoreDetailsByContributionType(ctx context.Context, contributionType string) (ContributionScore, error) - FetchUserContributions(ctx context.Context) ([]Contribution, error) + FetchUserContributions(ctx context.Context) ([]FetchUserContributionsResponse, error) GetContributionByGithubEventId(ctx context.Context, githubEventId string) (Contribution, error) ListMonthlyContributionSummary(ctx context.Context, year int, monthParam int, userId int) ([]MonthlyContributionSummary, error) } @@ -264,16 +264,23 @@ func (s *service) GetContributionScoreDetailsByContributionType(ctx context.Cont return ContributionScore(contributionScoreDetails), nil } -func (s *service) FetchUserContributions(ctx context.Context) ([]Contribution, error) { +func (s *service) FetchUserContributions(ctx context.Context) ([]FetchUserContributionsResponse, error) { userContributions, err := s.contributionRepository.FetchUserContributions(ctx, nil) if err != nil { slog.Error("error occured while fetching user contributions", "error", err) return nil, err } - serviceContributions := make([]Contribution, len(userContributions)) + serviceContributions := make([]FetchUserContributionsResponse, len(userContributions)) for i, c := range userContributions { - serviceContributions[i] = Contribution((c)) + serviceContributions[i].Contribution = Contribution(c) + fetchContributedRepository, err := s.repositoryService.GetRepoByRepoId(ctx, c.RepositoryId) + if err != nil { + slog.Error("error occured while fetching users contributed repository details", "error", err) + return nil, err + } + + serviceContributions[i].Repository = Repository(fetchContributedRepository) } return serviceContributions, nil diff --git a/backend/internal/app/dependencies.go b/backend/internal/app/dependencies.go index bc4fa575..91510b77 100644 --- a/backend/internal/app/dependencies.go +++ b/backend/internal/app/dependencies.go @@ -39,13 +39,13 @@ func InitDependencies(db *sqlx.DB, appCfg config.AppConfig, client config.Bigque repositoryRepository := repository.NewRepositoryRepository(db) transactionRepository := repository.NewTransactionRepository(db) + githubService := github.NewService(appCfg, httpClient) + repositoryService := repoService.NewService(repositoryRepository, githubService) badgeService := badge.NewService(badgeRepository) goalService := goal.NewService(goalRepository, contributionRepository, badgeService) - userService := user.NewService(userRepository, goalService) + userService := user.NewService(userRepository, goalService, repositoryService) authService := auth.NewService(userService, appCfg) bigqueryService := bigquery.NewService(client, userRepository) - githubService := github.NewService(appCfg, httpClient) - repositoryService := repoService.NewService(repositoryRepository, githubService) transactionService := transaction.NewService(transactionRepository, userService) contributionService := contribution.NewService(bigqueryService, contributionRepository, repositoryService, userService, transactionService, httpClient) diff --git a/backend/internal/app/github/domain.go b/backend/internal/app/github/domain.go index efdd4415..388db665 100644 --- a/backend/internal/app/github/domain.go +++ b/backend/internal/app/github/domain.go @@ -12,11 +12,11 @@ type FetchRepositoryDetailsResponse struct { Id int `json:"id"` Name string `json:"name"` Description string `json:"description"` - LanguagesURL string `json:"languages_url"` - UpdateDate time.Time `json:"updated_at"` + LanguagesURL string `json:"languagesUrl"` + UpdateDate time.Time `json:"updatedAt"` RepoOwnerName RepoOwner `json:"owner"` - ContributorsUrl string `json:"contributors_url"` - RepoUrl string `json:"html_url"` + ContributorsUrl string `json:"contributorsUrl"` + RepoUrl string `json:"repoUrl"` } type RepoLanguages map[string]int @@ -24,7 +24,7 @@ type RepoLanguages map[string]int type FetchRepoContributorsResponse struct { Id int `json:"id"` Name string `json:"login"` - AvatarUrl string `json:"avatar_url"` - GithubUrl string `json:"html_url"` + AvatarUrl string `json:"avatarUrl"` + GithubUrl string `json:"githubUrl"` Contributions int `json:"contributions"` } diff --git a/backend/internal/app/goal/domain.go b/backend/internal/app/goal/domain.go index 163d6157..e822e84f 100644 --- a/backend/internal/app/goal/domain.go +++ b/backend/internal/app/goal/domain.go @@ -3,24 +3,24 @@ package goal import "time" type Goal struct { - Id int - Level string - CreatedAt time.Time - UpdatedAt time.Time + Id int `json:"id"` + Level string `json:"level"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` } type GoalContribution struct { - Id int - GoalId int - ContributionScoreId int - TargetCount int - IsCustom bool - SetByUserId int - CreatedAt time.Time - UpdatedAt time.Time + Id int `json:"id"` + GoalId int `json:"goalId"` + ContributionScoreId int `json:"contributionScoreId"` + TargetCount int `json:"targetCount"` + IsCustom bool `json:"isCustom"` + SetByUserId int `json:"setByUserId"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` } type CustomGoalLevelTarget struct { - ContributionType string `json:"contribution_type"` + ContributionType string `json:"contributionType"` Target int `json:"target"` } diff --git a/backend/internal/app/repository/domain.go b/backend/internal/app/repository/domain.go index 208f2976..3e76c3ee 100644 --- a/backend/internal/app/repository/domain.go +++ b/backend/internal/app/repository/domain.go @@ -3,54 +3,54 @@ package repository import "time" type Repository struct { - Id int - GithubRepoId int - RepoName string - Description string - LanguagesUrl string - RepoUrl string - OwnerName string - UpdateDate time.Time - ContributorsUrl string - CreatedAt time.Time - UpdatedAt time.Time + Id int `json:"id"` + GithubRepoId int `json:"githubRepoId"` + RepoName string `json:"repoName"` + Description string `json:"description"` + LanguagesUrl string `json:"languagesUrl"` + RepoUrl string `json:"repoUrl"` + OwnerName string `json:"ownerName"` + UpdateDate time.Time `json:"updateDate"` + ContributorsUrl string `json:"contributorsUrl"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` } type RepoLanguages map[string]int type FetchUsersContributedReposResponse struct { Repository - Languages []string - TotalCoinsEarned int + Languages []string `json:"languages"` + TotalCoinsEarned int `json:"totalCoinsEarned"` } type ContributionResponse struct { - ID string `bigquery:"id"` - Type string `bigquery:"type"` - ActorID int `bigquery:"actor_id"` - ActorLogin string `bigquery:"actor_login"` - RepoID int `bigquery:"repo_id"` - RepoName string `bigquery:"repo_name"` - RepoUrl string `bigquery:"repo_url"` - Payload string `bigquery:"payload"` - CreatedAt time.Time `bigquery:"created_at"` + ID string `bigquery:"id" json:"id"` + Type string `bigquery:"type" json:"type"` + ActorID int `bigquery:"actor_id" json:"actorId"` + ActorLogin string `bigquery:"actor_login" json:"actorLogin"` + RepoID int `bigquery:"repo_id" json:"repoId"` + RepoName string `bigquery:"repo_name" json:"repoName"` + RepoUrl string `bigquery:"repo_url" json:"repoUrl"` + Payload string `bigquery:"payload" json:"payload"` + CreatedAt time.Time `bigquery:"created_at" json:"createdAt"` } type Contribution struct { - Id int - UserId int - RepositoryId int - ContributionScoreId int - ContributionType string - BalanceChange int - ContributedAt time.Time - GithubEventId string - CreatedAt time.Time - UpdatedAt time.Time + Id int `json:"id"` + UserId int `json:"userId"` + RepositoryId int `json:"repositoryId"` + ContributionScoreId int `json:"contributionScoreId"` + ContributionType string `json:"contributionType"` + BalanceChange int `json:"balanceChange"` + ContributedAt time.Time `json:"contributedAt"` + GithubEventId string `json:"githubEventId"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` } type LanguagePercent struct { - Name string - Bytes int - Percentage float64 + Name string `json:"name"` + Bytes int `json:"bytes"` + Percentage float64 `json:"percentage"` } diff --git a/backend/internal/app/repository/service.go b/backend/internal/app/repository/service.go index e73b75f3..71b127dd 100644 --- a/backend/internal/app/repository/service.go +++ b/backend/internal/app/repository/service.go @@ -24,6 +24,7 @@ type Service interface { FetchUsersContributedRepos(ctx context.Context, client *http.Client) ([]FetchUsersContributedReposResponse, error) FetchUserContributionsInRepo(ctx context.Context, githubRepoId int) ([]Contribution, error) CalculateLanguagePercentInRepo(ctx context.Context, repoLanguages RepoLanguages) ([]LanguagePercent, error) + FetchUserContributedReposCount(ctx context.Context, userId int) (int, error) } func NewService(repositoryRepository repository.RepositoryRepository, githubService github.Service) Service { @@ -165,3 +166,13 @@ func (s *service) CalculateLanguagePercentInRepo(ctx context.Context, repoLangua return langPercent, nil } + +func (s *service) FetchUserContributedReposCount(ctx context.Context, userId int) (int, error) { + userContributedReposCount, err := s.repositoryRepository.FetchUserContributedReposCount(ctx, nil, userId) + if err != nil { + slog.Error("error fetching users contributes repos count", "error", err) + return 0, err + } + + return userContributedReposCount, nil +} diff --git a/backend/internal/app/transaction/domain.go b/backend/internal/app/transaction/domain.go index 6a02f131..fa5a2055 100644 --- a/backend/internal/app/transaction/domain.go +++ b/backend/internal/app/transaction/domain.go @@ -3,26 +3,26 @@ package transaction import "time" type Transaction struct { - Id int - UserId int - ContributionId int - IsRedeemed bool - IsGained bool - TransactedBalance int - TransactedAt time.Time - CreatedAt time.Time - UpdatedAt time.Time + Id int `json:"id"` + UserId int `json:"userId"` + ContributionId int `json:"contributionId"` + IsRedeemed bool `json:"isRedeemed"` + IsGained bool `json:"isGained"` + TransactedBalance int `json:"transactedBalance"` + TransactedAt time.Time `json:"transactedAt"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` } type Contribution struct { - Id int - UserId int - RepositoryId int - ContributionScoreId int - ContributionType string - BalanceChange int - ContributedAt time.Time - GithubEventId string - CreatedAt time.Time - UpdatedAt time.Time + Id int `json:"id"` + UserId int `json:"userId"` + RepositoryId int `json:"repositoryId"` + ContributionScoreId int `json:"contributionScoreId"` + ContributionType string `json:"contributionType"` + BalanceChange int `json:"balanceChange"` + ContributedAt time.Time `json:"contributedAt"` + GithubEventId string `json:"githubEventId"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` } diff --git a/backend/internal/app/user/domain.go b/backend/internal/app/user/domain.go index 087f25f4..4599fe80 100644 --- a/backend/internal/app/user/domain.go +++ b/backend/internal/app/user/domain.go @@ -6,28 +6,28 @@ import ( ) type User struct { - Id int `json:"user_id"` - GithubId int `json:"github_id"` - GithubUsername string `json:"github_username"` + Id int `json:"userId"` + GithubId int `json:"githubId"` + GithubUsername string `json:"githubUsername"` Email string `json:"email"` - AvatarUrl string `json:"avatar_url"` - CurrentBalance int `json:"current_balance"` - CurrentActiveGoalId sql.NullInt64 `json:"current_active_goal_id"` - IsBlocked bool `json:"is_blocked"` - IsAdmin bool `json:"is_admin"` + AvatarUrl string `json:"avatarUrl"` + CurrentBalance int `json:"currentBalance"` + CurrentActiveGoalId sql.NullInt64 `json:"currentActiveGoalId"` + IsBlocked bool `json:"isBlocked"` + IsAdmin bool `json:"isAdmin"` Password string `json:"password"` - IsDeleted bool `json:"is_deleted"` - DeletedAt sql.NullTime `json:"deleted_at"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + IsDeleted bool `json:"isDeleted"` + DeletedAt sql.NullTime `json:"deletedAt"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` } type CreateUserRequestBody struct { - GithubId int `json:"id"` - GithubUsername string `json:"github_id"` - AvatarUrl string `json:"avatar_url"` + GithubId int `json:"githubId"` + GithubUsername string `json:"githubUsername"` + AvatarUrl string `json:"avatarUrl"` Email string `json:"email"` - IsAdmin bool `json:"is_admin"` + IsAdmin bool `json:"isAdmin"` } type Email struct { @@ -35,23 +35,24 @@ type Email struct { } type Transaction struct { - Id int - UserId int - ContributionId int - IsRedeemed bool - IsGained bool - TransactedBalance int - TransactedAt time.Time - CreatedAt time.Time - UpdatedAt time.Time + Id int `json:"id"` + UserId int `json:"userId"` + ContributionId int `json:"contributionId"` + IsRedeemed bool `json:"isRedeemed"` + IsGained bool `json:"isGained"` + TransactedBalance int `json:"transactedBalance"` + TransactedAt time.Time `json:"transactedAt"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` } type LeaderboardUser struct { - Id int - GithubUsername string - AvatarUrl string - CurrentBalance int - Rank int + Id int `json:"id"` + GithubUsername string `json:"githubUsername"` + AvatarUrl string `json:"avatarUrl"` + ContributedReposCount int `json:"contributedReposCount"` + CurrentBalance int `json:"currentBalance"` + Rank int `json:"rank"` } type GoalLevel struct { diff --git a/backend/internal/app/user/service.go b/backend/internal/app/user/service.go index 2b01e821..339d6c95 100644 --- a/backend/internal/app/user/service.go +++ b/backend/internal/app/user/service.go @@ -6,14 +6,16 @@ import ( "time" "github.com/joshsoftware/code-curiosity-2025/internal/app/goal" + repoService "github.com/joshsoftware/code-curiosity-2025/internal/app/repository" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/middleware" "github.com/joshsoftware/code-curiosity-2025/internal/repository" ) type service struct { - userRepository repository.UserRepository - goalService goal.Service + userRepository repository.UserRepository + goalService goal.Service + repositoryService repoService.Service } type Service interface { @@ -30,10 +32,11 @@ type Service interface { UpdateCurrentActiveGoalId(ctx context.Context, userId int, level string) (int, error) } -func NewService(userRepository repository.UserRepository, goalService goal.Service) Service { +func NewService(userRepository repository.UserRepository, goalService goal.Service, repositoryService repoService.Service) Service { return &service{ - userRepository: userRepository, - goalService: goalService, + userRepository: userRepository, + goalService: goalService, + repositoryService: repositoryService, } } @@ -147,7 +150,18 @@ func (s *service) GetAllUsersRank(ctx context.Context) ([]LeaderboardUser, error Leaderboard := make([]LeaderboardUser, len(userRanks)) for i, l := range userRanks { - Leaderboard[i] = LeaderboardUser(l) + userContributedReposCount, err := s.repositoryService.FetchUserContributedReposCount(ctx, l.Id) + if err != nil { + slog.Error("error fetching user contributed repos count", "error", err) + return nil, err + } + + Leaderboard[i].Id = l.Id + Leaderboard[i].GithubUsername = l.GithubUsername + Leaderboard[i].ContributedReposCount = userContributedReposCount + Leaderboard[i].AvatarUrl = l.AvatarUrl + Leaderboard[i].Rank = l.Rank + Leaderboard[i].CurrentBalance = l.CurrentBalance } return Leaderboard, nil @@ -160,7 +174,21 @@ func (s *service) GetCurrentUserRank(ctx context.Context, userId int) (Leaderboa return LeaderboardUser{}, err } - return LeaderboardUser(currentUserRank), nil + currentUserContributedReposCount, err := s.repositoryService.FetchUserContributedReposCount(ctx, userId) + if err != nil { + slog.Error("error fetching user contributed repos count", "error", err) + return LeaderboardUser{}, err + } + + leaderboardUser := LeaderboardUser{ + Id: currentUserRank.Id, + GithubUsername: currentUserRank.GithubUsername, + AvatarUrl: currentUserRank.AvatarUrl, + ContributedReposCount: currentUserContributedReposCount, + CurrentBalance: currentUserRank.CurrentBalance, + Rank: currentUserRank.Rank, + } + return leaderboardUser, nil } func (s *service) UpdateCurrentActiveGoalId(ctx context.Context, userId int, level string) (int, error) { diff --git a/backend/internal/db/migrations/1748862201_init.up.sql b/backend/internal/db/migrations/1748862201_init.up.sql index c06fb1d7..da1ebc85 100644 --- a/backend/internal/db/migrations/1748862201_init.up.sql +++ b/backend/internal/db/migrations/1748862201_init.up.sql @@ -3,7 +3,7 @@ CREATE TABLE "users"( "github_id" BIGINT NOT NULL UNIQUE, "github_username" VARCHAR(255) NOT NULL, "avatar_url" VARCHAR(255) NOT NULL, - "email" VARCHAR(255) NULL, + "email" VARCHAR(255) NULL DEFAULT '', "current_active_goal_id" BIGINT NULL, "current_balance" BIGINT DEFAULT 0, "is_blocked" BOOLEAN DEFAULT FALSE, diff --git a/backend/internal/db/migrations/1752476063_create-index-users-current-balance.up.sql b/backend/internal/db/migrations/1752476063_create-index-users-current-balance.up.sql index a934bf1b..60222f22 100644 --- a/backend/internal/db/migrations/1752476063_create-index-users-current-balance.up.sql +++ b/backend/internal/db/migrations/1752476063_create-index-users-current-balance.up.sql @@ -1 +1,3 @@ -CREATE INDEX idx_users_current_balance ON users(current_balance DESC); \ No newline at end of file +CREATE INDEX idx_users_current_balance +ON users(current_balance DESC) +WHERE is_admin = false AND is_deleted = false; diff --git a/backend/internal/pkg/apperrors/errors.go b/backend/internal/pkg/apperrors/errors.go index 26069af4..c1e60415 100644 --- a/backend/internal/pkg/apperrors/errors.go +++ b/backend/internal/pkg/apperrors/errors.go @@ -32,11 +32,12 @@ var ( ErrJWTCreationFailed = errors.New("failed to create jwt token") ErrAuthorizationFailed = errors.New("failed to authorize user") - ErrRepoNotFound = errors.New("repository not found") - ErrRepoCreationFailed = errors.New("failed to create repo for user") - ErrCalculatingUserRepoTotalCoins = errors.New("error calculating total coins earned by user for the repository") - ErrFetchingUsersContributedRepos = errors.New("error fetching users contributed repositories") - ErrFetchingUserContributionsInRepo = errors.New("error fetching users contribution in repository") + ErrRepoNotFound = errors.New("repository not found") + ErrRepoCreationFailed = errors.New("failed to create repo for user") + ErrCalculatingUserRepoTotalCoins = errors.New("error calculating total coins earned by user for the repository") + ErrFetchingUsersContributedRepos = errors.New("error fetching users contributed repositories") + ErrFetchingUserContributionsInRepo = errors.New("error fetching users contribution in repository") + ErrFetchingUsersContributedReposCount = errors.New("error fetching user contributed repos count") ErrFetchingFromBigquery = errors.New("error fetching contributions from bigquery service") ErrNextContribution = errors.New("error while loading next bigquery contribution") diff --git a/backend/internal/repository/repository.go b/backend/internal/repository/repository.go index 96a1d91f..97079d1f 100644 --- a/backend/internal/repository/repository.go +++ b/backend/internal/repository/repository.go @@ -23,6 +23,7 @@ type RepositoryRepository interface { GetUserRepoTotalCoins(ctx context.Context, tx *sqlx.Tx, repoId int) (int, error) FetchUsersContributedRepos(ctx context.Context, tx *sqlx.Tx) ([]Repository, error) FetchUserContributionsInRepo(ctx context.Context, tx *sqlx.Tx, repoGithubId int) ([]Contribution, error) + FetchUserContributedReposCount(ctx context.Context, tx *sqlx.Tx, userId int) (int, error) } func NewRepositoryRepository(db *sqlx.DB) RepositoryRepository { @@ -55,6 +56,8 @@ const ( fetchUsersContributedReposQuery = `SELECT * from repositories where id in (SELECT repository_id from contributions where user_id=$1);` fetchUserContributionsInRepoQuery = `SELECT * from contributions where repository_id=$1 and user_id=$2;` + + fetchUserContributedReposCountQuery = `SELECT COUNT(DISTINCT repository_id) AS unique_repo_count FROM contributions WHERE user_id = $1;` ) func (rr *repositoryRepository) GetRepoByGithubId(ctx context.Context, tx *sqlx.Tx, repoGithubId int) (Repository, error) { @@ -178,3 +181,16 @@ func (r *repositoryRepository) FetchUserContributionsInRepo(ctx context.Context, return userContributionsInRepo, nil } + +func (r *repositoryRepository) FetchUserContributedReposCount(ctx context.Context, tx *sqlx.Tx, userId int) (int, error) { + executer := r.BaseRepository.initiateQueryExecuter(tx) + + var usersContributedReposCount int + err := executer.GetContext(ctx, &usersContributedReposCount, fetchUserContributedReposCountQuery, userId) + if err != nil { + slog.Error("error fetching user contributed repos count", "error", err) + return 0, apperrors.ErrFetchingUsersContributedReposCount + } + + return usersContributedReposCount, nil +} diff --git a/backend/internal/repository/user.go b/backend/internal/repository/user.go index 15b1f6a1..49f1a0bc 100644 --- a/backend/internal/repository/user.go +++ b/backend/internal/repository/user.go @@ -72,6 +72,8 @@ const ( current_balance, RANK() over (ORDER BY current_balance DESC) AS rank FROM users + WHERE is_admin=false + AND is_deleted=false ORDER BY current_balance DESC` getCurrentUserRankQuery = ` diff --git a/backend/run-backend.sh b/backend/run-backend.sh old mode 100644 new mode 100755 index 3ca49127..b5175fd3 --- a/backend/run-backend.sh +++ b/backend/run-backend.sh @@ -1,4 +1,4 @@ export CONFIG_PATH=local.yaml -export GOOGLE_APPLICATION_CREDENTIALS=/home/josh/Documents/Codez/cobalt-alliance-459708-h5-3e1df629b2ff.json +export GOOGLE_APPLICATION_CREDENTIALS=/home/josh/Documents/cobalt-alliance-459708-h5-3e1df629b2ff.json go run ./cmd/main.go \ No newline at end of file diff --git a/backend/run-migrations-up.sh b/backend/run-migrations-up.sh new file mode 100755 index 00000000..21f004af --- /dev/null +++ b/backend/run-migrations-up.sh @@ -0,0 +1,3 @@ +export CONFIG_PATH=local.yaml + +go run ./internal/db/migrate.go up \ No newline at end of file diff --git a/frontend/yarn.lock b/frontend/yarn.lock deleted file mode 100644 index d1791a55..00000000 --- a/frontend/yarn.lock +++ /dev/null @@ -1,2047 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@ampproject/remapping@^2.2.0", "@ampproject/remapping@^2.3.0": - version "2.3.0" - resolved "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz" - integrity sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw== - dependencies: - "@jridgewell/gen-mapping" "^0.3.5" - "@jridgewell/trace-mapping" "^0.3.24" - -"@babel/code-frame@^7.27.1": - version "7.27.1" - resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz" - integrity sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg== - dependencies: - "@babel/helper-validator-identifier" "^7.27.1" - js-tokens "^4.0.0" - picocolors "^1.1.1" - -"@babel/compat-data@^7.27.2": - version "7.28.0" - resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz" - integrity sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw== - -"@babel/core@^7.0.0", "@babel/core@^7.0.0-0", "@babel/core@^7.28.0": - version "7.28.0" - resolved "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz" - integrity sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ== - dependencies: - "@ampproject/remapping" "^2.2.0" - "@babel/code-frame" "^7.27.1" - "@babel/generator" "^7.28.0" - "@babel/helper-compilation-targets" "^7.27.2" - "@babel/helper-module-transforms" "^7.27.3" - "@babel/helpers" "^7.27.6" - "@babel/parser" "^7.28.0" - "@babel/template" "^7.27.2" - "@babel/traverse" "^7.28.0" - "@babel/types" "^7.28.0" - convert-source-map "^2.0.0" - debug "^4.1.0" - gensync "^1.0.0-beta.2" - json5 "^2.2.3" - semver "^6.3.1" - -"@babel/generator@^7.28.0": - version "7.28.0" - resolved "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz" - integrity sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg== - dependencies: - "@babel/parser" "^7.28.0" - "@babel/types" "^7.28.0" - "@jridgewell/gen-mapping" "^0.3.12" - "@jridgewell/trace-mapping" "^0.3.28" - jsesc "^3.0.2" - -"@babel/helper-compilation-targets@^7.27.2": - version "7.27.2" - resolved "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz" - integrity sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ== - dependencies: - "@babel/compat-data" "^7.27.2" - "@babel/helper-validator-option" "^7.27.1" - browserslist "^4.24.0" - lru-cache "^5.1.1" - semver "^6.3.1" - -"@babel/helper-globals@^7.28.0": - version "7.28.0" - resolved "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz" - integrity sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw== - -"@babel/helper-module-imports@^7.27.1": - version "7.27.1" - resolved "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz" - integrity sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w== - dependencies: - "@babel/traverse" "^7.27.1" - "@babel/types" "^7.27.1" - -"@babel/helper-module-transforms@^7.27.3": - version "7.27.3" - resolved "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz" - integrity sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg== - dependencies: - "@babel/helper-module-imports" "^7.27.1" - "@babel/helper-validator-identifier" "^7.27.1" - "@babel/traverse" "^7.27.3" - -"@babel/helper-plugin-utils@^7.27.1": - version "7.27.1" - resolved "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz" - integrity sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw== - -"@babel/helper-string-parser@^7.27.1": - version "7.27.1" - resolved "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz" - integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA== - -"@babel/helper-validator-identifier@^7.27.1": - version "7.27.1" - resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz" - integrity sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow== - -"@babel/helper-validator-option@^7.27.1": - version "7.27.1" - resolved "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz" - integrity sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg== - -"@babel/helpers@^7.27.6": - version "7.27.6" - resolved "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz" - integrity sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug== - dependencies: - "@babel/template" "^7.27.2" - "@babel/types" "^7.27.6" - -"@babel/parser@^7.1.0", "@babel/parser@^7.20.7", "@babel/parser@^7.27.2", "@babel/parser@^7.28.0": - version "7.28.0" - resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz" - integrity sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g== - dependencies: - "@babel/types" "^7.28.0" - -"@babel/plugin-transform-react-jsx-self@^7.27.1": - version "7.27.1" - resolved "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz" - integrity sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw== - dependencies: - "@babel/helper-plugin-utils" "^7.27.1" - -"@babel/plugin-transform-react-jsx-source@^7.27.1": - version "7.27.1" - resolved "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz" - integrity sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw== - dependencies: - "@babel/helper-plugin-utils" "^7.27.1" - -"@babel/template@^7.27.2": - version "7.27.2" - resolved "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz" - integrity sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw== - dependencies: - "@babel/code-frame" "^7.27.1" - "@babel/parser" "^7.27.2" - "@babel/types" "^7.27.1" - -"@babel/traverse@^7.27.1", "@babel/traverse@^7.27.3", "@babel/traverse@^7.28.0": - version "7.28.0" - resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz" - integrity sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg== - dependencies: - "@babel/code-frame" "^7.27.1" - "@babel/generator" "^7.28.0" - "@babel/helper-globals" "^7.28.0" - "@babel/parser" "^7.28.0" - "@babel/template" "^7.27.2" - "@babel/types" "^7.28.0" - debug "^4.3.1" - -"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.27.1", "@babel/types@^7.27.6", "@babel/types@^7.28.0": - version "7.28.1" - resolved "https://registry.npmjs.org/@babel/types/-/types-7.28.1.tgz" - integrity sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ== - dependencies: - "@babel/helper-string-parser" "^7.27.1" - "@babel/helper-validator-identifier" "^7.27.1" - -"@esbuild/linux-x64@0.25.6": - version "0.25.6" - resolved "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.6.tgz" - integrity sha512-A6bJB41b4lKFWRKNrWoP2LHsjVzNiaurf7wyj/XtFNTsnPuxwEBWHLty+ZE0dWBKuSK1fvKgrKaNjBS7qbFKig== - -"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.7.0": - version "4.7.0" - resolved "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz" - integrity sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw== - dependencies: - eslint-visitor-keys "^3.4.3" - -"@eslint-community/regexpp@^4.10.0", "@eslint-community/regexpp@^4.12.1": - version "4.12.1" - resolved "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz" - integrity sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ== - -"@eslint/config-array@^0.21.0": - version "0.21.0" - resolved "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz" - integrity sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ== - dependencies: - "@eslint/object-schema" "^2.1.6" - debug "^4.3.1" - minimatch "^3.1.2" - -"@eslint/config-helpers@^0.3.0": - version "0.3.0" - resolved "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz" - integrity sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw== - -"@eslint/core@^0.15.0", "@eslint/core@^0.15.1": - version "0.15.1" - resolved "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz" - integrity sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA== - dependencies: - "@types/json-schema" "^7.0.15" - -"@eslint/eslintrc@^3.3.1": - version "3.3.1" - resolved "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz" - integrity sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ== - dependencies: - ajv "^6.12.4" - debug "^4.3.2" - espree "^10.0.1" - globals "^14.0.0" - ignore "^5.2.0" - import-fresh "^3.2.1" - js-yaml "^4.1.0" - minimatch "^3.1.2" - strip-json-comments "^3.1.1" - -"@eslint/js@^9.29.0", "@eslint/js@9.31.0": - version "9.31.0" - resolved "https://registry.npmjs.org/@eslint/js/-/js-9.31.0.tgz" - integrity sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw== - -"@eslint/object-schema@^2.1.6": - version "2.1.6" - resolved "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz" - integrity sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA== - -"@eslint/plugin-kit@^0.3.1": - version "0.3.3" - resolved "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.3.tgz" - integrity sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag== - dependencies: - "@eslint/core" "^0.15.1" - levn "^0.4.1" - -"@humanfs/core@^0.19.1": - version "0.19.1" - resolved "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz" - integrity sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA== - -"@humanfs/node@^0.16.6": - version "0.16.6" - resolved "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz" - integrity sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw== - dependencies: - "@humanfs/core" "^0.19.1" - "@humanwhocodes/retry" "^0.3.0" - -"@humanwhocodes/module-importer@^1.0.1": - version "1.0.1" - resolved "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz" - integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== - -"@humanwhocodes/retry@^0.3.0": - version "0.3.1" - resolved "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz" - integrity sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA== - -"@humanwhocodes/retry@^0.4.2": - version "0.4.3" - resolved "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz" - integrity sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ== - -"@isaacs/fs-minipass@^4.0.0": - version "4.0.1" - resolved "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz" - integrity sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w== - dependencies: - minipass "^7.0.4" - -"@jridgewell/gen-mapping@^0.3.12", "@jridgewell/gen-mapping@^0.3.5": - version "0.3.12" - resolved "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz" - integrity sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg== - dependencies: - "@jridgewell/sourcemap-codec" "^1.5.0" - "@jridgewell/trace-mapping" "^0.3.24" - -"@jridgewell/resolve-uri@^3.1.0": - version "3.1.2" - resolved "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz" - integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== - -"@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0": - version "1.5.4" - resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz" - integrity sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw== - -"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.28": - version "0.3.29" - resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz" - integrity sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ== - dependencies: - "@jridgewell/resolve-uri" "^3.1.0" - "@jridgewell/sourcemap-codec" "^1.4.14" - -"@nodelib/fs.scandir@2.1.5": - version "2.1.5" - resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz" - integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== - dependencies: - "@nodelib/fs.stat" "2.0.5" - run-parallel "^1.1.9" - -"@nodelib/fs.stat@^2.0.2", "@nodelib/fs.stat@2.0.5": - version "2.0.5" - resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz" - integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== - -"@nodelib/fs.walk@^1.2.3": - version "1.2.8" - resolved "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz" - integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== - dependencies: - "@nodelib/fs.scandir" "2.1.5" - fastq "^1.6.0" - -"@pkgr/core@^0.2.4": - version "0.2.7" - resolved "https://registry.npmjs.org/@pkgr/core/-/core-0.2.7.tgz" - integrity sha512-YLT9Zo3oNPJoBjBc4q8G2mjU4tqIbf5CEOORbUUr48dCD9q3umJ3IPlVqOqDakPfd2HuwccBaqlGhN4Gmr5OWg== - -"@radix-ui/react-compose-refs@1.1.2": - version "1.1.2" - resolved "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz" - integrity sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg== - -"@radix-ui/react-slot@^1.2.3": - version "1.2.3" - resolved "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz" - integrity sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A== - dependencies: - "@radix-ui/react-compose-refs" "1.1.2" - -"@rolldown/pluginutils@1.0.0-beta.27": - version "1.0.0-beta.27" - resolved "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz" - integrity sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA== - -"@rollup/rollup-linux-x64-gnu@4.45.1": - version "4.45.1" - resolved "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.45.1.tgz" - integrity sha512-+E/lYl6qu1zqgPEnTrs4WysQtvc/Sh4fC2nByfFExqgYrqkKWp1tWIbe+ELhixnenSpBbLXNi6vbEEJ8M7fiHw== - -"@rollup/rollup-linux-x64-musl@4.45.1": - version "4.45.1" - resolved "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.45.1.tgz" - integrity sha512-a6WIAp89p3kpNoYStITT9RbTbTnqarU7D8N8F2CV+4Cl9fwCOZraLVuVFvlpsW0SbIiYtEnhCZBPLoNdRkjQFw== - -"@tailwindcss/node@4.1.11": - version "4.1.11" - resolved "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.11.tgz" - integrity sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q== - dependencies: - "@ampproject/remapping" "^2.3.0" - enhanced-resolve "^5.18.1" - jiti "^2.4.2" - lightningcss "1.30.1" - magic-string "^0.30.17" - source-map-js "^1.2.1" - tailwindcss "4.1.11" - -"@tailwindcss/oxide-linux-x64-gnu@4.1.11": - version "4.1.11" - resolved "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.11.tgz" - integrity sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg== - -"@tailwindcss/oxide-linux-x64-musl@4.1.11": - version "4.1.11" - resolved "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.11.tgz" - integrity sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q== - -"@tailwindcss/oxide@4.1.11": - version "4.1.11" - resolved "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.11.tgz" - integrity sha512-Q69XzrtAhuyfHo+5/HMgr1lAiPP/G40OMFAnws7xcFEYqcypZmdW8eGXaOUIeOl1dzPJBPENXgbjsOyhg2nkrg== - dependencies: - detect-libc "^2.0.4" - tar "^7.4.3" - optionalDependencies: - "@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.11": - version "4.1.11" - resolved "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.11.tgz" - integrity sha512-RHYhrR3hku0MJFRV+fN2gNbDNEh3dwKvY8XJvTxCSXeMOsCRSr+uKvDWQcbizrHgjML6ZmTE5OwMrl5wKcujCw== - dependencies: - "@tailwindcss/node" "4.1.11" - "@tailwindcss/oxide" "4.1.11" - tailwindcss "4.1.11" - -"@tanstack/query-core@5.83.0": - version "5.83.0" - resolved "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.83.0.tgz" - integrity sha512-0M8dA+amXUkyz5cVUm/B+zSk3xkQAcuXuz5/Q/LveT4ots2rBpPTZOzd7yJa2Utsf8D2Upl5KyjhHRY+9lB/XA== - -"@tanstack/query-devtools@5.81.2": - version "5.81.2" - resolved "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.81.2.tgz" - integrity sha512-jCeJcDCwKfoyyBXjXe9+Lo8aTkavygHHsUHAlxQKKaDeyT0qyQNLKl7+UyqYH2dDF6UN/14873IPBHchcsU+Zg== - -"@tanstack/react-query-devtools@^5.83.0": - version "5.83.0" - resolved "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.83.0.tgz" - integrity sha512-yfp8Uqd3I1jgx8gl0lxbSSESu5y4MO2ThOPBnGNTYs0P+ZFu+E9g5IdOngyUGuo6Uz6Qa7p9TLdZEX3ntik2fQ== - dependencies: - "@tanstack/query-devtools" "5.81.2" - -"@tanstack/react-query@^5.83.0": - version "5.83.0" - resolved "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.83.0.tgz" - integrity sha512-/XGYhZ3foc5H0VM2jLSD/NyBRIOK4q9kfeml4+0x2DlL6xVuAcVEW+hTlTapAmejObg0i3eNqhkr2dT+eciwoQ== - dependencies: - "@tanstack/query-core" "5.83.0" - -"@types/babel__core@^7.20.5": - version "7.20.5" - resolved "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz" - integrity sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA== - dependencies: - "@babel/parser" "^7.20.7" - "@babel/types" "^7.20.7" - "@types/babel__generator" "*" - "@types/babel__template" "*" - "@types/babel__traverse" "*" - -"@types/babel__generator@*": - version "7.27.0" - resolved "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz" - integrity sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg== - dependencies: - "@babel/types" "^7.0.0" - -"@types/babel__template@*": - version "7.4.4" - resolved "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz" - integrity sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A== - dependencies: - "@babel/parser" "^7.1.0" - "@babel/types" "^7.0.0" - -"@types/babel__traverse@*": - version "7.20.7" - resolved "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz" - integrity sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng== - dependencies: - "@babel/types" "^7.20.7" - -"@types/estree@^1.0.6", "@types/estree@1.0.8": - version "1.0.8" - resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz" - integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w== - -"@types/json-schema@^7.0.15": - version "7.0.15" - resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz" - integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== - -"@types/node@^20.19.0 || >=22.12.0", "@types/node@^24.0.15": - version "24.0.15" - resolved "https://registry.npmjs.org/@types/node/-/node-24.0.15.tgz" - integrity sha512-oaeTSbCef7U/z7rDeJA138xpG3NuKc64/rZ2qmUFkFJmnMsAPaluIifqyWd8hSSMxyP9oie3dLAqYPblag9KgA== - dependencies: - undici-types "~7.8.0" - -"@types/react-dom@^19.1.6": - version "19.1.6" - resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.6.tgz" - integrity sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw== - -"@types/react@*", "@types/react@^19.0.0", "@types/react@^19.1.8": - version "19.1.8" - resolved "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz" - integrity sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g== - dependencies: - csstype "^3.0.2" - -"@typescript-eslint/eslint-plugin@8.37.0": - version "8.37.0" - resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.37.0.tgz" - integrity sha512-jsuVWeIkb6ggzB+wPCsR4e6loj+rM72ohW6IBn2C+5NCvfUVY8s33iFPySSVXqtm5Hu29Ne/9bnA0JmyLmgenA== - dependencies: - "@eslint-community/regexpp" "^4.10.0" - "@typescript-eslint/scope-manager" "8.37.0" - "@typescript-eslint/type-utils" "8.37.0" - "@typescript-eslint/utils" "8.37.0" - "@typescript-eslint/visitor-keys" "8.37.0" - graphemer "^1.4.0" - ignore "^7.0.0" - natural-compare "^1.4.0" - ts-api-utils "^2.1.0" - -"@typescript-eslint/parser@^8.37.0", "@typescript-eslint/parser@8.37.0": - version "8.37.0" - resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.37.0.tgz" - integrity sha512-kVIaQE9vrN9RLCQMQ3iyRlVJpTiDUY6woHGb30JDkfJErqrQEmtdWH3gV0PBAfGZgQXoqzXOO0T3K6ioApbbAA== - dependencies: - "@typescript-eslint/scope-manager" "8.37.0" - "@typescript-eslint/types" "8.37.0" - "@typescript-eslint/typescript-estree" "8.37.0" - "@typescript-eslint/visitor-keys" "8.37.0" - debug "^4.3.4" - -"@typescript-eslint/project-service@8.37.0": - version "8.37.0" - resolved "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.37.0.tgz" - integrity sha512-BIUXYsbkl5A1aJDdYJCBAo8rCEbAvdquQ8AnLb6z5Lp1u3x5PNgSSx9A/zqYc++Xnr/0DVpls8iQ2cJs/izTXA== - dependencies: - "@typescript-eslint/tsconfig-utils" "^8.37.0" - "@typescript-eslint/types" "^8.37.0" - debug "^4.3.4" - -"@typescript-eslint/scope-manager@8.37.0": - version "8.37.0" - resolved "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.37.0.tgz" - integrity sha512-0vGq0yiU1gbjKob2q691ybTg9JX6ShiVXAAfm2jGf3q0hdP6/BruaFjL/ManAR/lj05AvYCH+5bbVo0VtzmjOA== - dependencies: - "@typescript-eslint/types" "8.37.0" - "@typescript-eslint/visitor-keys" "8.37.0" - -"@typescript-eslint/tsconfig-utils@^8.37.0", "@typescript-eslint/tsconfig-utils@8.37.0": - version "8.37.0" - resolved "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.37.0.tgz" - integrity sha512-1/YHvAVTimMM9mmlPvTec9NP4bobA1RkDbMydxG8omqwJJLEW/Iy2C4adsAESIXU3WGLXFHSZUU+C9EoFWl4Zg== - -"@typescript-eslint/type-utils@8.37.0": - version "8.37.0" - resolved "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.37.0.tgz" - integrity sha512-SPkXWIkVZxhgwSwVq9rqj/4VFo7MnWwVaRNznfQDc/xPYHjXnPfLWn+4L6FF1cAz6e7dsqBeMawgl7QjUMj4Ow== - dependencies: - "@typescript-eslint/types" "8.37.0" - "@typescript-eslint/typescript-estree" "8.37.0" - "@typescript-eslint/utils" "8.37.0" - debug "^4.3.4" - ts-api-utils "^2.1.0" - -"@typescript-eslint/types@^8.37.0", "@typescript-eslint/types@8.37.0": - version "8.37.0" - resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.37.0.tgz" - integrity sha512-ax0nv7PUF9NOVPs+lmQ7yIE7IQmAf8LGcXbMvHX5Gm+YJUYNAl340XkGnrimxZ0elXyoQJuN5sbg6C4evKA4SQ== - -"@typescript-eslint/typescript-estree@8.37.0": - version "8.37.0" - resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.37.0.tgz" - integrity sha512-zuWDMDuzMRbQOM+bHyU4/slw27bAUEcKSKKs3hcv2aNnc/tvE/h7w60dwVw8vnal2Pub6RT1T7BI8tFZ1fE+yg== - dependencies: - "@typescript-eslint/project-service" "8.37.0" - "@typescript-eslint/tsconfig-utils" "8.37.0" - "@typescript-eslint/types" "8.37.0" - "@typescript-eslint/visitor-keys" "8.37.0" - debug "^4.3.4" - fast-glob "^3.3.2" - is-glob "^4.0.3" - minimatch "^9.0.4" - semver "^7.6.0" - ts-api-utils "^2.1.0" - -"@typescript-eslint/utils@8.37.0": - version "8.37.0" - resolved "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.37.0.tgz" - integrity sha512-TSFvkIW6gGjN2p6zbXo20FzCABbyUAuq6tBvNRGsKdsSQ6a7rnV6ADfZ7f4iI3lIiXc4F4WWvtUfDw9CJ9pO5A== - dependencies: - "@eslint-community/eslint-utils" "^4.7.0" - "@typescript-eslint/scope-manager" "8.37.0" - "@typescript-eslint/types" "8.37.0" - "@typescript-eslint/typescript-estree" "8.37.0" - -"@typescript-eslint/visitor-keys@8.37.0": - version "8.37.0" - resolved "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.37.0.tgz" - integrity sha512-YzfhzcTnZVPiLfP/oeKtDp2evwvHLMe0LOy7oe+hb9KKIumLNohYS9Hgp1ifwpu42YWxhZE8yieggz6JpqO/1w== - dependencies: - "@typescript-eslint/types" "8.37.0" - eslint-visitor-keys "^4.2.1" - -"@vitejs/plugin-react@^4.7.0": - version "4.7.0" - resolved "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz" - integrity sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA== - dependencies: - "@babel/core" "^7.28.0" - "@babel/plugin-transform-react-jsx-self" "^7.27.1" - "@babel/plugin-transform-react-jsx-source" "^7.27.1" - "@rolldown/pluginutils" "1.0.0-beta.27" - "@types/babel__core" "^7.20.5" - react-refresh "^0.17.0" - -acorn-jsx@^5.3.2: - version "5.3.2" - resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" - integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== - -"acorn@^6.0.0 || ^7.0.0 || ^8.0.0", acorn@^8.15.0: - version "8.15.0" - resolved "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz" - integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg== - -ajv@^6.12.4: - version "6.12.6" - resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz" - integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== - dependencies: - fast-deep-equal "^3.1.1" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.4.1" - uri-js "^4.2.2" - -ansi-escapes@^7.0.0: - version "7.0.0" - resolved "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz" - integrity sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw== - dependencies: - environment "^1.0.0" - -ansi-regex@^6.0.1: - version "6.1.0" - resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz" - integrity sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA== - -ansi-styles@^4.1.0: - version "4.3.0" - resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz" - integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== - dependencies: - color-convert "^2.0.1" - -ansi-styles@^6.0.0: - version "6.2.1" - resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz" - integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== - -ansi-styles@^6.2.1: - version "6.2.1" - resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz" - integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== - -argparse@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz" - integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== - -asynckit@^0.4.0: - version "0.4.0" - resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz" - integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== - -axios@^1.10.0: - version "1.10.0" - resolved "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz" - integrity sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw== - dependencies: - follow-redirects "^1.15.6" - form-data "^4.0.0" - proxy-from-env "^1.1.0" - -balanced-match@^1.0.0: - version "1.0.2" - resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" - integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== - -brace-expansion@^1.1.7: - version "1.1.12" - resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz" - integrity sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg== - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - -brace-expansion@^2.0.1: - version "2.0.2" - resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz" - integrity sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ== - dependencies: - balanced-match "^1.0.0" - -braces@^3.0.3: - version "3.0.3" - resolved "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz" - integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== - dependencies: - fill-range "^7.1.1" - -browserslist@^4.24.0, "browserslist@>= 4.21.0": - version "4.25.1" - resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz" - integrity sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw== - dependencies: - caniuse-lite "^1.0.30001726" - electron-to-chromium "^1.5.173" - node-releases "^2.0.19" - update-browserslist-db "^1.1.3" - -call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz" - integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== - dependencies: - es-errors "^1.3.0" - function-bind "^1.1.2" - -callsites@^3.0.0: - version "3.1.0" - resolved "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz" - integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== - -caniuse-lite@^1.0.30001726: - version "1.0.30001727" - resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz" - integrity sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q== - -chalk@^4.0.0: - version "4.1.2" - resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" - integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== - dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" - -chalk@^5.4.1: - version "5.4.1" - resolved "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz" - integrity sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w== - -chownr@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz" - integrity sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g== - -class-variance-authority@^0.7.1: - version "0.7.1" - resolved "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz" - integrity sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg== - dependencies: - clsx "^2.1.1" - -cli-cursor@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz" - integrity sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw== - dependencies: - restore-cursor "^5.0.0" - -cli-truncate@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz" - integrity sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA== - dependencies: - slice-ansi "^5.0.0" - string-width "^7.0.0" - -clsx@^2.1.1: - version "2.1.1" - resolved "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz" - integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA== - -color-convert@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz" - integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== - dependencies: - color-name "~1.1.4" - -color-name@~1.1.4: - version "1.1.4" - resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" - integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== - -colorette@^2.0.20: - version "2.0.20" - resolved "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz" - integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== - -combined-stream@^1.0.8: - version "1.0.8" - resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz" - integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== - dependencies: - delayed-stream "~1.0.0" - -commander@^14.0.0: - version "14.0.0" - resolved "https://registry.npmjs.org/commander/-/commander-14.0.0.tgz" - integrity sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA== - -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" - integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== - -convert-source-map@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz" - integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== - -cookie@^1.0.1: - version "1.0.2" - resolved "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz" - integrity sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA== - -cross-spawn@^7.0.6: - version "7.0.6" - resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz" - integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== - dependencies: - path-key "^3.1.0" - shebang-command "^2.0.0" - which "^2.0.1" - -csstype@^3.0.2: - version "3.1.3" - resolved "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz" - integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== - -debug@^4.1.0, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.4.1: - version "4.4.1" - resolved "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz" - integrity sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ== - dependencies: - ms "^2.1.3" - -deep-is@^0.1.3: - version "0.1.4" - resolved "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz" - integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== - -delayed-stream@~1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" - integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== - -detect-libc@^2.0.3, detect-libc@^2.0.4: - version "2.0.4" - resolved "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz" - integrity sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA== - -dotenv@^17.2.0: - version "17.2.0" - resolved "https://registry.npmjs.org/dotenv/-/dotenv-17.2.0.tgz" - integrity sha512-Q4sgBT60gzd0BB0lSyYD3xM4YxrXA9y4uBDof1JNYGzOXrQdQ6yX+7XIAqoFOGQFOTK1D3Hts5OllpxMDZFONQ== - -dunder-proto@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz" - integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== - dependencies: - call-bind-apply-helpers "^1.0.1" - es-errors "^1.3.0" - gopd "^1.2.0" - -electron-to-chromium@^1.5.173: - version "1.5.187" - resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.187.tgz" - integrity sha512-cl5Jc9I0KGUoOoSbxvTywTa40uspGJt/BDBoDLoxJRSBpWh4FFXBsjNRHfQrONsV/OoEjDfHUmZQa2d6Ze4YgA== - -emoji-regex@^10.3.0: - version "10.4.0" - resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz" - integrity sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw== - -enhanced-resolve@^5.18.1: - version "5.18.2" - resolved "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz" - integrity sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ== - dependencies: - graceful-fs "^4.2.4" - tapable "^2.2.0" - -environment@^1.0.0: - version "1.1.0" - resolved "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz" - integrity sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q== - -es-define-property@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz" - integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== - -es-errors@^1.3.0: - version "1.3.0" - resolved "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz" - integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== - -es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz" - integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA== - dependencies: - es-errors "^1.3.0" - -es-set-tostringtag@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz" - integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA== - dependencies: - es-errors "^1.3.0" - get-intrinsic "^1.2.6" - has-tostringtag "^1.0.2" - hasown "^2.0.2" - -esbuild@^0.25.0: - version "0.25.6" - resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.25.6.tgz" - integrity sha512-GVuzuUwtdsghE3ocJ9Bs8PNoF13HNQ5TXbEi2AhvVb8xU1Iwt9Fos9FEamfoee+u/TOsn7GUWc04lz46n2bbTg== - optionalDependencies: - "@esbuild/aix-ppc64" "0.25.6" - "@esbuild/android-arm" "0.25.6" - "@esbuild/android-arm64" "0.25.6" - "@esbuild/android-x64" "0.25.6" - "@esbuild/darwin-arm64" "0.25.6" - "@esbuild/darwin-x64" "0.25.6" - "@esbuild/freebsd-arm64" "0.25.6" - "@esbuild/freebsd-x64" "0.25.6" - "@esbuild/linux-arm" "0.25.6" - "@esbuild/linux-arm64" "0.25.6" - "@esbuild/linux-ia32" "0.25.6" - "@esbuild/linux-loong64" "0.25.6" - "@esbuild/linux-mips64el" "0.25.6" - "@esbuild/linux-ppc64" "0.25.6" - "@esbuild/linux-riscv64" "0.25.6" - "@esbuild/linux-s390x" "0.25.6" - "@esbuild/linux-x64" "0.25.6" - "@esbuild/netbsd-arm64" "0.25.6" - "@esbuild/netbsd-x64" "0.25.6" - "@esbuild/openbsd-arm64" "0.25.6" - "@esbuild/openbsd-x64" "0.25.6" - "@esbuild/openharmony-arm64" "0.25.6" - "@esbuild/sunos-x64" "0.25.6" - "@esbuild/win32-arm64" "0.25.6" - "@esbuild/win32-ia32" "0.25.6" - "@esbuild/win32-x64" "0.25.6" - -escalade@^3.2.0: - version "3.2.0" - resolved "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz" - integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== - -escape-string-regexp@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz" - integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== - -eslint-config-prettier@^10.1.5, "eslint-config-prettier@>= 7.0.0 <10.0.0 || >=10.1.0": - version "10.1.5" - resolved "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.5.tgz" - integrity sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw== - -eslint-plugin-prettier@^5.5.1: - version "5.5.1" - resolved "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.1.tgz" - integrity sha512-dobTkHT6XaEVOo8IO90Q4DOSxnm3Y151QxPJlM/vKC0bVy+d6cVWQZLlFiuZPP0wS6vZwSKeJgKkcS+KfMBlRw== - dependencies: - prettier-linter-helpers "^1.0.0" - synckit "^0.11.7" - -eslint-plugin-react-hooks@^5.2.0: - version "5.2.0" - resolved "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz" - integrity sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg== - -eslint-plugin-react-refresh@^0.4.20: - version "0.4.20" - resolved "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.20.tgz" - integrity sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA== - -eslint-scope@^8.4.0: - version "8.4.0" - resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz" - integrity sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg== - dependencies: - esrecurse "^4.3.0" - estraverse "^5.2.0" - -eslint-visitor-keys@^3.4.3: - version "3.4.3" - resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz" - integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== - -eslint-visitor-keys@^4.2.1: - version "4.2.1" - resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz" - integrity sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ== - -"eslint@^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0", "eslint@^6.0.0 || ^7.0.0 || >=8.0.0", "eslint@^8.57.0 || ^9.0.0", eslint@^9.31.0, eslint@>=7.0.0, eslint@>=8.0.0, eslint@>=8.40: - version "9.31.0" - resolved "https://registry.npmjs.org/eslint/-/eslint-9.31.0.tgz" - integrity sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ== - dependencies: - "@eslint-community/eslint-utils" "^4.2.0" - "@eslint-community/regexpp" "^4.12.1" - "@eslint/config-array" "^0.21.0" - "@eslint/config-helpers" "^0.3.0" - "@eslint/core" "^0.15.0" - "@eslint/eslintrc" "^3.3.1" - "@eslint/js" "9.31.0" - "@eslint/plugin-kit" "^0.3.1" - "@humanfs/node" "^0.16.6" - "@humanwhocodes/module-importer" "^1.0.1" - "@humanwhocodes/retry" "^0.4.2" - "@types/estree" "^1.0.6" - "@types/json-schema" "^7.0.15" - ajv "^6.12.4" - chalk "^4.0.0" - cross-spawn "^7.0.6" - debug "^4.3.2" - escape-string-regexp "^4.0.0" - eslint-scope "^8.4.0" - eslint-visitor-keys "^4.2.1" - espree "^10.4.0" - esquery "^1.5.0" - esutils "^2.0.2" - fast-deep-equal "^3.1.3" - file-entry-cache "^8.0.0" - find-up "^5.0.0" - glob-parent "^6.0.2" - ignore "^5.2.0" - imurmurhash "^0.1.4" - is-glob "^4.0.0" - json-stable-stringify-without-jsonify "^1.0.1" - lodash.merge "^4.6.2" - minimatch "^3.1.2" - natural-compare "^1.4.0" - optionator "^0.9.3" - -espree@^10.0.1, espree@^10.4.0: - version "10.4.0" - resolved "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz" - integrity sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ== - dependencies: - acorn "^8.15.0" - acorn-jsx "^5.3.2" - eslint-visitor-keys "^4.2.1" - -esquery@^1.5.0: - version "1.6.0" - resolved "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz" - integrity sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg== - dependencies: - estraverse "^5.1.0" - -esrecurse@^4.3.0: - version "4.3.0" - resolved "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz" - integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== - dependencies: - estraverse "^5.2.0" - -estraverse@^5.1.0, estraverse@^5.2.0: - version "5.3.0" - resolved "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz" - integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== - -esutils@^2.0.2: - version "2.0.3" - resolved "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz" - integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== - -eventemitter3@^5.0.1: - version "5.0.1" - resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz" - integrity sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA== - -fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: - version "3.1.3" - resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" - integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== - -fast-diff@^1.1.2: - version "1.3.0" - resolved "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz" - integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw== - -fast-glob@^3.3.2: - version "3.3.3" - resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz" - integrity sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg== - dependencies: - "@nodelib/fs.stat" "^2.0.2" - "@nodelib/fs.walk" "^1.2.3" - glob-parent "^5.1.2" - merge2 "^1.3.0" - micromatch "^4.0.8" - -fast-json-stable-stringify@^2.0.0: - version "2.1.0" - resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz" - integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== - -fast-levenshtein@^2.0.6: - version "2.0.6" - resolved "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz" - integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== - -fastq@^1.6.0: - version "1.19.1" - resolved "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz" - integrity sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ== - dependencies: - reusify "^1.0.4" - -fdir@^6.4.4: - version "6.4.6" - resolved "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz" - integrity sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w== - -fdir@^6.4.6: - version "6.4.6" - resolved "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz" - integrity sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w== - -file-entry-cache@^8.0.0: - version "8.0.0" - resolved "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz" - integrity sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ== - dependencies: - flat-cache "^4.0.0" - -fill-range@^7.1.1: - version "7.1.1" - resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz" - integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== - dependencies: - to-regex-range "^5.0.1" - -find-up@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz" - integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== - dependencies: - locate-path "^6.0.0" - path-exists "^4.0.0" - -flat-cache@^4.0.0: - version "4.0.1" - resolved "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz" - integrity sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw== - dependencies: - flatted "^3.2.9" - keyv "^4.5.4" - -flatted@^3.2.9: - version "3.3.3" - resolved "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz" - integrity sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg== - -follow-redirects@^1.15.6: - version "1.15.9" - resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz" - integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ== - -form-data@^4.0.0: - version "4.0.4" - resolved "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz" - integrity sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.8" - es-set-tostringtag "^2.1.0" - hasown "^2.0.2" - mime-types "^2.1.12" - -function-bind@^1.1.2: - version "1.1.2" - resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz" - integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== - -gensync@^1.0.0-beta.2: - version "1.0.0-beta.2" - resolved "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz" - integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== - -get-east-asian-width@^1.0.0: - version "1.3.0" - resolved "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz" - integrity sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ== - -get-intrinsic@^1.2.6: - version "1.3.0" - resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz" - integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== - dependencies: - call-bind-apply-helpers "^1.0.2" - es-define-property "^1.0.1" - es-errors "^1.3.0" - es-object-atoms "^1.1.1" - function-bind "^1.1.2" - get-proto "^1.0.1" - gopd "^1.2.0" - has-symbols "^1.1.0" - hasown "^2.0.2" - math-intrinsics "^1.1.0" - -get-proto@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz" - integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== - dependencies: - dunder-proto "^1.0.1" - es-object-atoms "^1.0.0" - -glob-parent@^5.1.2: - version "5.1.2" - resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz" - integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== - dependencies: - is-glob "^4.0.1" - -glob-parent@^6.0.2: - version "6.0.2" - resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz" - integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== - dependencies: - is-glob "^4.0.3" - -globals@^14.0.0: - version "14.0.0" - resolved "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz" - integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ== - -globals@^16.2.0: - version "16.3.0" - resolved "https://registry.npmjs.org/globals/-/globals-16.3.0.tgz" - integrity sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ== - -gopd@^1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz" - integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== - -graceful-fs@^4.2.4: - version "4.2.11" - resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz" - integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== - -graphemer@^1.4.0: - version "1.4.0" - resolved "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz" - integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== - -has-flag@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz" - integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== - -has-symbols@^1.0.3, has-symbols@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz" - integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== - -has-tostringtag@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz" - integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== - dependencies: - has-symbols "^1.0.3" - -hasown@^2.0.2: - version "2.0.2" - resolved "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz" - integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== - dependencies: - function-bind "^1.1.2" - -husky@^8.0.0: - version "8.0.3" - resolved "https://registry.npmjs.org/husky/-/husky-8.0.3.tgz" - integrity sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg== - -ignore@^5.2.0: - version "5.3.2" - resolved "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz" - integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== - -ignore@^7.0.0: - version "7.0.5" - resolved "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz" - integrity sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg== - -import-fresh@^3.2.1: - version "3.3.1" - resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz" - integrity sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ== - dependencies: - parent-module "^1.0.0" - resolve-from "^4.0.0" - -imurmurhash@^0.1.4: - version "0.1.4" - resolved "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz" - integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== - -is-extglob@^2.1.1: - version "2.1.1" - resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz" - integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== - -is-fullwidth-code-point@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz" - integrity sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ== - -is-fullwidth-code-point@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz" - integrity sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA== - dependencies: - get-east-asian-width "^1.0.0" - -is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3: - version "4.0.3" - resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz" - integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== - dependencies: - is-extglob "^2.1.1" - -is-number@^7.0.0: - version "7.0.0" - resolved "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz" - integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== - -isexe@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" - integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== - -jiti@*, jiti@^2.4.2, jiti@>=1.21.0: - version "2.4.2" - resolved "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz" - integrity sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A== - -js-tokens@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz" - integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== - -js-yaml@^4.1.0: - version "4.1.0" - resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz" - integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== - dependencies: - argparse "^2.0.1" - -jsesc@^3.0.2: - version "3.1.0" - resolved "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz" - integrity sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA== - -json-buffer@3.0.1: - version "3.0.1" - resolved "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz" - integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== - -json-schema-traverse@^0.4.1: - version "0.4.1" - resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz" - integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== - -json-stable-stringify-without-jsonify@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz" - integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== - -json5@^2.2.3: - version "2.2.3" - resolved "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz" - integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== - -keyv@^4.5.4: - version "4.5.4" - resolved "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz" - integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== - dependencies: - json-buffer "3.0.1" - -levn@^0.4.1: - version "0.4.1" - resolved "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz" - integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== - dependencies: - prelude-ls "^1.2.1" - type-check "~0.4.0" - -lightningcss-linux-x64-gnu@1.30.1: - version "1.30.1" - resolved "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz" - integrity sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw== - -lightningcss-linux-x64-musl@1.30.1: - version "1.30.1" - resolved "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz" - integrity sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ== - -lightningcss@^1.21.0, lightningcss@1.30.1: - version "1.30.1" - resolved "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz" - integrity sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg== - dependencies: - detect-libc "^2.0.3" - optionalDependencies: - lightningcss-darwin-arm64 "1.30.1" - lightningcss-darwin-x64 "1.30.1" - lightningcss-freebsd-x64 "1.30.1" - lightningcss-linux-arm-gnueabihf "1.30.1" - lightningcss-linux-arm64-gnu "1.30.1" - lightningcss-linux-arm64-musl "1.30.1" - lightningcss-linux-x64-gnu "1.30.1" - lightningcss-linux-x64-musl "1.30.1" - lightningcss-win32-arm64-msvc "1.30.1" - lightningcss-win32-x64-msvc "1.30.1" - -lilconfig@^3.1.3: - version "3.1.3" - resolved "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz" - integrity sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw== - -lint-staged@^16.1.2: - version "16.1.2" - resolved "https://registry.npmjs.org/lint-staged/-/lint-staged-16.1.2.tgz" - integrity sha512-sQKw2Si2g9KUZNY3XNvRuDq4UJqpHwF0/FQzZR2M7I5MvtpWvibikCjUVJzZdGE0ByurEl3KQNvsGetd1ty1/Q== - dependencies: - chalk "^5.4.1" - commander "^14.0.0" - debug "^4.4.1" - lilconfig "^3.1.3" - listr2 "^8.3.3" - micromatch "^4.0.8" - nano-spawn "^1.0.2" - pidtree "^0.6.0" - string-argv "^0.3.2" - yaml "^2.8.0" - -listr2@^8.3.3: - version "8.3.3" - resolved "https://registry.npmjs.org/listr2/-/listr2-8.3.3.tgz" - integrity sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ== - dependencies: - cli-truncate "^4.0.0" - colorette "^2.0.20" - eventemitter3 "^5.0.1" - log-update "^6.1.0" - rfdc "^1.4.1" - wrap-ansi "^9.0.0" - -locate-path@^6.0.0: - version "6.0.0" - resolved "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz" - integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== - dependencies: - p-locate "^5.0.0" - -lodash.merge@^4.6.2: - version "4.6.2" - resolved "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz" - integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== - -log-update@^6.1.0: - version "6.1.0" - resolved "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz" - integrity sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w== - dependencies: - ansi-escapes "^7.0.0" - cli-cursor "^5.0.0" - slice-ansi "^7.1.0" - strip-ansi "^7.1.0" - wrap-ansi "^9.0.0" - -lru-cache@^5.1.1: - version "5.1.1" - resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz" - integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== - dependencies: - yallist "^3.0.2" - -lucide-react@^0.525.0: - version "0.525.0" - resolved "https://registry.npmjs.org/lucide-react/-/lucide-react-0.525.0.tgz" - integrity sha512-Tm1txJ2OkymCGkvwoHt33Y2JpN5xucVq1slHcgE6Lk0WjDfjgKWor5CdVER8U6DvcfMwh4M8XxmpTiyzfmfDYQ== - -magic-string@^0.30.17: - version "0.30.17" - resolved "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz" - integrity sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA== - dependencies: - "@jridgewell/sourcemap-codec" "^1.5.0" - -math-intrinsics@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz" - integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== - -merge2@^1.3.0: - version "1.4.1" - resolved "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz" - integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== - -micromatch@^4.0.8: - version "4.0.8" - resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz" - integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== - dependencies: - braces "^3.0.3" - picomatch "^2.3.1" - -mime-db@1.52.0: - version "1.52.0" - resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz" - integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== - -mime-types@^2.1.12: - version "2.1.35" - resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz" - integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== - dependencies: - mime-db "1.52.0" - -mimic-function@^5.0.0: - version "5.0.1" - resolved "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz" - integrity sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA== - -minimatch@^3.1.2: - version "3.1.2" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" - integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== - dependencies: - brace-expansion "^1.1.7" - -minimatch@^9.0.4: - version "9.0.5" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz" - integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== - dependencies: - brace-expansion "^2.0.1" - -minipass@^7.0.4, minipass@^7.1.2: - version "7.1.2" - resolved "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz" - integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== - -minizlib@^3.0.1: - version "3.0.2" - resolved "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz" - integrity sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA== - dependencies: - minipass "^7.1.2" - -mkdirp@^3.0.1: - version "3.0.1" - resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz" - integrity sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg== - -ms@^2.1.3: - version "2.1.3" - resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" - integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== - -nano-spawn@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/nano-spawn/-/nano-spawn-1.0.2.tgz" - integrity sha512-21t+ozMQDAL/UGgQVBbZ/xXvNO10++ZPuTmKRO8k9V3AClVRht49ahtDjfY8l1q6nSHOrE5ASfthzH3ol6R/hg== - -nanoid@^3.3.11: - version "3.3.11" - resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz" - integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w== - -natural-compare@^1.4.0: - version "1.4.0" - resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz" - integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== - -node-releases@^2.0.19: - version "2.0.19" - resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz" - integrity sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw== - -onetime@^7.0.0: - version "7.0.0" - resolved "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz" - integrity sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ== - dependencies: - mimic-function "^5.0.0" - -optionator@^0.9.3: - version "0.9.4" - resolved "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz" - integrity sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g== - dependencies: - deep-is "^0.1.3" - fast-levenshtein "^2.0.6" - levn "^0.4.1" - prelude-ls "^1.2.1" - type-check "^0.4.0" - word-wrap "^1.2.5" - -p-limit@^3.0.2: - version "3.1.0" - resolved "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz" - integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== - dependencies: - yocto-queue "^0.1.0" - -p-locate@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz" - integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== - dependencies: - p-limit "^3.0.2" - -parent-module@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz" - integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== - dependencies: - callsites "^3.0.0" - -path-exists@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz" - integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== - -path-key@^3.1.0: - version "3.1.1" - resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz" - integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== - -picocolors@^1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz" - integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== - -picomatch@^2.3.1: - version "2.3.1" - resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" - integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== - -"picomatch@^3 || ^4", picomatch@^4.0.2: - version "4.0.3" - resolved "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz" - integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q== - -pidtree@^0.6.0: - version "0.6.0" - resolved "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz" - integrity sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g== - -postcss@^8.5.6: - version "8.5.6" - resolved "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz" - integrity sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg== - dependencies: - nanoid "^3.3.11" - picocolors "^1.1.1" - source-map-js "^1.2.1" - -prelude-ls@^1.2.1: - version "1.2.1" - resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz" - integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== - -prettier-linter-helpers@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz" - integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w== - dependencies: - fast-diff "^1.1.2" - -prettier@^3.6.2, prettier@>=3.0.0: - version "3.6.2" - resolved "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz" - integrity sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ== - -proxy-from-env@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz" - integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== - -punycode@^2.1.0: - version "2.3.1" - resolved "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz" - integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== - -queue-microtask@^1.2.2: - version "1.2.3" - resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz" - integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== - -"react-dom@^18 || ^19", react-dom@^19.1.0, react-dom@>=18: - version "19.1.0" - resolved "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz" - integrity sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g== - dependencies: - scheduler "^0.26.0" - -react-refresh@^0.17.0: - version "0.17.0" - resolved "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz" - integrity sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ== - -react-router-dom@^7.7.0: - version "7.7.0" - resolved "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.7.0.tgz" - integrity sha512-wwGS19VkNBkneVh9/YD0pK3IsjWxQUVMDD6drlG7eJpo1rXBtctBqDyBm/k+oKHRAm1x9XWT3JFC82QI9YOXXA== - dependencies: - react-router "7.7.0" - -react-router@7.7.0: - version "7.7.0" - resolved "https://registry.npmjs.org/react-router/-/react-router-7.7.0.tgz" - integrity sha512-3FUYSwlvB/5wRJVTL/aavqHmfUKe0+Xm9MllkYgGo9eDwNdkvwlJGjpPxono1kCycLt6AnDTgjmXvK3/B4QGuw== - dependencies: - cookie "^1.0.1" - set-cookie-parser "^2.6.0" - -react-toastify@^11.0.5: - version "11.0.5" - resolved "https://registry.npmjs.org/react-toastify/-/react-toastify-11.0.5.tgz" - integrity sha512-EpqHBGvnSTtHYhCPLxML05NLY2ZX0JURbAdNYa6BUkk+amz4wbKBQvoKQAB0ardvSarUBuY4Q4s1sluAzZwkmA== - dependencies: - clsx "^2.1.1" - -"react@^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react@^18 || ^19", react@^19.1.0, react@>=18: - version "19.1.0" - resolved "https://registry.npmjs.org/react/-/react-19.1.0.tgz" - integrity sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg== - -resolve-from@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz" - integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== - -restore-cursor@^5.0.0: - version "5.1.0" - resolved "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz" - integrity sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA== - dependencies: - onetime "^7.0.0" - signal-exit "^4.1.0" - -reusify@^1.0.4: - version "1.1.0" - resolved "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz" - integrity sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw== - -rfdc@^1.4.1: - version "1.4.1" - resolved "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz" - integrity sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA== - -rollup@^4.40.0: - version "4.45.1" - resolved "https://registry.npmjs.org/rollup/-/rollup-4.45.1.tgz" - integrity sha512-4iya7Jb76fVpQyLoiVpzUrsjQ12r3dM7fIVz+4NwoYvZOShknRmiv+iu9CClZml5ZLGb0XMcYLutK6w9tgxHDw== - dependencies: - "@types/estree" "1.0.8" - optionalDependencies: - "@rollup/rollup-android-arm-eabi" "4.45.1" - "@rollup/rollup-android-arm64" "4.45.1" - "@rollup/rollup-darwin-arm64" "4.45.1" - "@rollup/rollup-darwin-x64" "4.45.1" - "@rollup/rollup-freebsd-arm64" "4.45.1" - "@rollup/rollup-freebsd-x64" "4.45.1" - "@rollup/rollup-linux-arm-gnueabihf" "4.45.1" - "@rollup/rollup-linux-arm-musleabihf" "4.45.1" - "@rollup/rollup-linux-arm64-gnu" "4.45.1" - "@rollup/rollup-linux-arm64-musl" "4.45.1" - "@rollup/rollup-linux-loongarch64-gnu" "4.45.1" - "@rollup/rollup-linux-powerpc64le-gnu" "4.45.1" - "@rollup/rollup-linux-riscv64-gnu" "4.45.1" - "@rollup/rollup-linux-riscv64-musl" "4.45.1" - "@rollup/rollup-linux-s390x-gnu" "4.45.1" - "@rollup/rollup-linux-x64-gnu" "4.45.1" - "@rollup/rollup-linux-x64-musl" "4.45.1" - "@rollup/rollup-win32-arm64-msvc" "4.45.1" - "@rollup/rollup-win32-ia32-msvc" "4.45.1" - "@rollup/rollup-win32-x64-msvc" "4.45.1" - fsevents "~2.3.2" - -run-parallel@^1.1.9: - version "1.2.0" - resolved "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz" - integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== - dependencies: - queue-microtask "^1.2.2" - -scheduler@^0.26.0: - version "0.26.0" - resolved "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz" - integrity sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA== - -semver@^6.3.1: - version "6.3.1" - resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz" - integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== - -semver@^7.6.0: - version "7.7.2" - resolved "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz" - integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA== - -set-cookie-parser@^2.6.0: - version "2.7.1" - resolved "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz" - integrity sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ== - -shebang-command@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz" - integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== - dependencies: - shebang-regex "^3.0.0" - -shebang-regex@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz" - integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== - -signal-exit@^4.1.0: - version "4.1.0" - resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz" - integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== - -slice-ansi@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz" - integrity sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ== - dependencies: - ansi-styles "^6.0.0" - is-fullwidth-code-point "^4.0.0" - -slice-ansi@^7.1.0: - version "7.1.0" - resolved "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz" - integrity sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg== - dependencies: - ansi-styles "^6.2.1" - is-fullwidth-code-point "^5.0.0" - -source-map-js@^1.2.1: - version "1.2.1" - resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz" - integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== - -string-argv@^0.3.2: - version "0.3.2" - resolved "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz" - integrity sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q== - -string-width@^7.0.0: - version "7.2.0" - resolved "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz" - integrity sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ== - dependencies: - emoji-regex "^10.3.0" - get-east-asian-width "^1.0.0" - strip-ansi "^7.1.0" - -strip-ansi@^7.1.0: - version "7.1.0" - resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz" - integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== - dependencies: - ansi-regex "^6.0.1" - -strip-json-comments@^3.1.1: - version "3.1.1" - resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz" - integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== - -supports-color@^7.1.0: - version "7.2.0" - resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz" - integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== - dependencies: - has-flag "^4.0.0" - -synckit@^0.11.7: - version "0.11.8" - resolved "https://registry.npmjs.org/synckit/-/synckit-0.11.8.tgz" - integrity sha512-+XZ+r1XGIJGeQk3VvXhT6xx/VpbHsRzsTkGgF6E5RX9TTXD0118l87puaEBZ566FhqblC6U0d4XnubznJDm30A== - dependencies: - "@pkgr/core" "^0.2.4" - -tailwind-merge@^3.3.1: - version "3.3.1" - resolved "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz" - integrity sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g== - -tailwindcss@^4.1.11, tailwindcss@4.1.11: - version "4.1.11" - resolved "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz" - integrity sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA== - -tapable@^2.2.0: - version "2.2.2" - resolved "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz" - integrity sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg== - -tar@^7.4.3: - version "7.4.3" - resolved "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz" - integrity sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw== - dependencies: - "@isaacs/fs-minipass" "^4.0.0" - chownr "^3.0.0" - minipass "^7.1.2" - minizlib "^3.0.1" - mkdirp "^3.0.1" - yallist "^5.0.0" - -tinyglobby@^0.2.14: - version "0.2.14" - resolved "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz" - integrity sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ== - dependencies: - fdir "^6.4.4" - picomatch "^4.0.2" - -to-regex-range@^5.0.1: - version "5.0.1" - resolved "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz" - integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== - dependencies: - is-number "^7.0.0" - -ts-api-utils@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz" - integrity sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ== - -tw-animate-css@^1.3.5: - version "1.3.5" - resolved "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.3.5.tgz" - integrity sha512-t3u+0YNoloIhj1mMXs779P6MO9q3p3mvGn4k1n3nJPqJw/glZcuijG2qTSN4z4mgNRfW5ZC3aXJFLwDtiipZXA== - -type-check@^0.4.0, type-check@~0.4.0: - version "0.4.0" - resolved "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz" - integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== - dependencies: - prelude-ls "^1.2.1" - -typescript-eslint@^8.34.1: - version "8.37.0" - resolved "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.37.0.tgz" - integrity sha512-TnbEjzkE9EmcO0Q2zM+GE8NQLItNAJpMmED1BdgoBMYNdqMhzlbqfdSwiRlAzEK2pA9UzVW0gzaaIzXWg2BjfA== - dependencies: - "@typescript-eslint/eslint-plugin" "8.37.0" - "@typescript-eslint/parser" "8.37.0" - "@typescript-eslint/typescript-estree" "8.37.0" - "@typescript-eslint/utils" "8.37.0" - -typescript@>=4.8.4, "typescript@>=4.8.4 <5.9.0", typescript@~5.8.3: - version "5.8.3" - resolved "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz" - integrity sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ== - -undici-types@~7.8.0: - version "7.8.0" - resolved "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz" - integrity sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw== - -update-browserslist-db@^1.1.3: - version "1.1.3" - resolved "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz" - integrity sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw== - dependencies: - escalade "^3.2.0" - picocolors "^1.1.1" - -uri-js@^4.2.2: - version "4.4.1" - resolved "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz" - integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== - dependencies: - punycode "^2.1.0" - -"vite@^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0", "vite@^5.2.0 || ^6 || ^7", vite@^7.0.0: - version "7.0.5" - resolved "https://registry.npmjs.org/vite/-/vite-7.0.5.tgz" - integrity sha512-1mncVwJxy2C9ThLwz0+2GKZyEXuC3MyWtAAlNftlZZXZDP3AJt5FmwcMit/IGGaNZ8ZOB2BNO/HFUB+CpN0NQw== - dependencies: - esbuild "^0.25.0" - fdir "^6.4.6" - picomatch "^4.0.2" - postcss "^8.5.6" - rollup "^4.40.0" - tinyglobby "^0.2.14" - optionalDependencies: - fsevents "~2.3.3" - -which@^2.0.1: - version "2.0.2" - resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz" - integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== - dependencies: - isexe "^2.0.0" - -word-wrap@^1.2.5: - version "1.2.5" - resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz" - integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== - -wrap-ansi@^9.0.0: - version "9.0.0" - resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz" - integrity sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q== - dependencies: - ansi-styles "^6.2.1" - string-width "^7.0.0" - strip-ansi "^7.1.0" - -yallist@^3.0.2: - version "3.1.1" - resolved "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz" - integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== - -yallist@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz" - integrity sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw== - -yaml@^2.4.2, yaml@^2.8.0: - version "2.8.0" - resolved "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz" - integrity sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ== - -yocto-queue@^0.1.0: - version "0.1.0" - resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz" - integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== From 00316febe7599c37fc93a4c2d548eb09705ed3d4 Mon Sep 17 00:00:00 2001 From: VishakhaSainani-Josh Date: Mon, 28 Jul 2025 01:23:01 +0530 Subject: [PATCH 08/36] refactor code --- backend/internal/app/github/domain.go | 4 ++-- backend/internal/app/repository/handler.go | 1 + backend/internal/app/router.go | 1 + backend/internal/pkg/utils/helper.go | 3 ++- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/backend/internal/app/github/domain.go b/backend/internal/app/github/domain.go index 388db665..e4ec6002 100644 --- a/backend/internal/app/github/domain.go +++ b/backend/internal/app/github/domain.go @@ -24,7 +24,7 @@ type RepoLanguages map[string]int type FetchRepoContributorsResponse struct { Id int `json:"id"` Name string `json:"login"` - AvatarUrl string `json:"avatarUrl"` - GithubUrl string `json:"githubUrl"` + AvatarUrl string `json:"avatar_url"` + GithubUrl string `json:"html_url"` Contributions int `json:"contributions"` } diff --git a/backend/internal/app/repository/handler.go b/backend/internal/app/repository/handler.go index f040a31c..91bc7c1e 100644 --- a/backend/internal/app/repository/handler.go +++ b/backend/internal/app/repository/handler.go @@ -20,6 +20,7 @@ type Handler interface { FetchParticularRepoDetails(w http.ResponseWriter, r *http.Request) FetchUserContributionsInRepo(w http.ResponseWriter, r *http.Request) FetchLanguagePercentInRepo(w http.ResponseWriter, r *http.Request) + FetchParticularRepoContributors(w http.ResponseWriter, r *http.Request) } func NewHandler(repositoryService Service, githubService github.Service) Handler { diff --git a/backend/internal/app/router.go b/backend/internal/app/router.go index c1bf5642..818d2cef 100644 --- a/backend/internal/app/router.go +++ b/backend/internal/app/router.go @@ -29,6 +29,7 @@ func NewRouter(deps Dependencies) http.Handler { router.HandleFunc("GET /api/v1/user/repositories/{repo_id}", middleware.Authentication(deps.RepositoryHandler.FetchParticularRepoDetails, deps.AppCfg)) router.HandleFunc("GET /api/v1/user/repositories/contributions/recent/{repo_id}", middleware.Authentication(deps.RepositoryHandler.FetchUserContributionsInRepo, deps.AppCfg)) router.HandleFunc("GET /api/v1/user/repositories/languages/{repo_id}", middleware.Authentication(deps.RepositoryHandler.FetchLanguagePercentInRepo, deps.AppCfg)) + router.HandleFunc("GET /api/v1/user/repositories/contributors/{repo_id}", middleware.Authentication(deps.RepositoryHandler.FetchParticularRepoContributors, deps.AppCfg)) router.HandleFunc("GET /api/v1/leaderboard", middleware.Authentication(deps.UserHandler.ListUserRanks, deps.AppCfg)) router.HandleFunc("GET /api/v1/user/leaderboard", middleware.Authentication(deps.UserHandler.GetCurrentUserRank, deps.AppCfg)) diff --git a/backend/internal/pkg/utils/helper.go b/backend/internal/pkg/utils/helper.go index e324c8dd..9f0d1882 100644 --- a/backend/internal/pkg/utils/helper.go +++ b/backend/internal/pkg/utils/helper.go @@ -29,7 +29,8 @@ func DoGet(httpClient *http.Client, url string, headers map[string]string) ([]by } for key, value := range headers { - req.Header.Add(key, value) + req.Header.Set(key, value) + fmt.Println("", key, "-", value) } resp, err := httpClient.Do(req) From de7bfe088e1d299b4101fe1ebf9eb5095d743d74 Mon Sep 17 00:00:00 2001 From: VishakhaSainani-Josh Date: Mon, 28 Jul 2025 12:39:26 +0530 Subject: [PATCH 09/36] remove print statement --- backend/internal/pkg/utils/helper.go | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/internal/pkg/utils/helper.go b/backend/internal/pkg/utils/helper.go index 9f0d1882..e024eec3 100644 --- a/backend/internal/pkg/utils/helper.go +++ b/backend/internal/pkg/utils/helper.go @@ -30,7 +30,6 @@ func DoGet(httpClient *http.Client, url string, headers map[string]string) ([]by for key, value := range headers { req.Header.Set(key, value) - fmt.Println("", key, "-", value) } resp, err := httpClient.Do(req) From 36a93e999d09829fa088b9e1944a78546bdcd803 Mon Sep 17 00:00:00 2001 From: VishakhaSainani-Josh Date: Mon, 28 Jul 2025 13:02:41 +0530 Subject: [PATCH 10/36] send languages array in response --- backend/internal/app/repository/domain.go | 5 ++++ backend/internal/app/repository/handler.go | 2 +- backend/internal/app/repository/service.go | 27 ++++++++++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/backend/internal/app/repository/domain.go b/backend/internal/app/repository/domain.go index 3e76c3ee..e9d86e30 100644 --- a/backend/internal/app/repository/domain.go +++ b/backend/internal/app/repository/domain.go @@ -24,6 +24,11 @@ type FetchUsersContributedReposResponse struct { TotalCoinsEarned int `json:"totalCoinsEarned"` } +type FetchParticularRepoDetailsResponse struct { + Repository + Languages []string `json:"languages"` +} + type ContributionResponse struct { ID string `bigquery:"id" json:"id"` Type string `bigquery:"type" json:"type"` diff --git a/backend/internal/app/repository/handler.go b/backend/internal/app/repository/handler.go index 91bc7c1e..d2e76e66 100644 --- a/backend/internal/app/repository/handler.go +++ b/backend/internal/app/repository/handler.go @@ -57,7 +57,7 @@ func (h *handler) FetchParticularRepoDetails(w http.ResponseWriter, r *http.Requ return } - repoDetails, err := h.repositoryService.GetRepoByRepoId(ctx, repoId) + repoDetails, err := h.repositoryService.FetchParticularRepoDetails(ctx, repoId) if err != nil { slog.Error("error fetching particular repo details", "error", err) status, errorMessage := apperrors.MapError(err) diff --git a/backend/internal/app/repository/service.go b/backend/internal/app/repository/service.go index 71b127dd..3aa127cf 100644 --- a/backend/internal/app/repository/service.go +++ b/backend/internal/app/repository/service.go @@ -22,6 +22,7 @@ type Service interface { CreateRepository(ctx context.Context, repoGithubId int, ContributionRepoDetailsUrl string) (Repository, error) HandleRepositoryCreation(ctx context.Context, contribution ContributionResponse) (Repository, error) FetchUsersContributedRepos(ctx context.Context, client *http.Client) ([]FetchUsersContributedReposResponse, error) + FetchParticularRepoDetails(ctx context.Context, repoId int) (FetchParticularRepoDetailsResponse, error) FetchUserContributionsInRepo(ctx context.Context, githubRepoId int) ([]Contribution, error) CalculateLanguagePercentInRepo(ctx context.Context, repoLanguages RepoLanguages) ([]LanguagePercent, error) FetchUserContributedReposCount(ctx context.Context, userId int) (int, error) @@ -132,6 +133,32 @@ func (s *service) FetchUsersContributedRepos(ctx context.Context, client *http.C return fetchUsersContributedReposResponse, nil } +func (s *service) FetchParticularRepoDetails(ctx context.Context, repoId int) (FetchParticularRepoDetailsResponse, error) { + repoDetails, err := s.GetRepoByRepoId(ctx, repoId) + if err != nil { + slog.Error("error getting repo by repo id", "error", err) + return FetchParticularRepoDetailsResponse{}, err + } + + repoLanguages, err := s.githubService.FetchRepositoryLanguages(ctx, repoDetails.LanguagesUrl) + if err != nil { + slog.Error("error fetching languages for repository", "error", err) + return FetchParticularRepoDetailsResponse{}, err + } + + var particularRepoLanguages []string + for language := range repoLanguages { + particularRepoLanguages = append(particularRepoLanguages, language) + } + + particularRepoDetails := FetchParticularRepoDetailsResponse{ + Repository: repoDetails, + Languages: particularRepoLanguages, + } + + return particularRepoDetails, nil +} + func (s *service) FetchUserContributionsInRepo(ctx context.Context, githubRepoId int) ([]Contribution, error) { userContributionsInRepo, err := s.repositoryRepository.FetchUserContributionsInRepo(ctx, nil, githubRepoId) if err != nil { From 5c769a2ff3787fe216728af1bbb35926599ff80a Mon Sep 17 00:00:00 2001 From: VishakhaSainani-Josh Date: Tue, 29 Jul 2025 16:58:10 +0530 Subject: [PATCH 11/36] refactor :- get context value in handler itself --- backend/internal/app/auth/handler.go | 16 +++++++-- backend/internal/app/auth/service.go | 13 ++----- backend/internal/app/contribution/handler.go | 11 +++++- backend/internal/app/contribution/service.go | 8 ++--- backend/internal/app/goal/service.go | 4 +-- backend/internal/app/repository/handler.go | 23 ++++++++++-- backend/internal/app/repository/service.go | 14 ++++---- backend/internal/app/router.go | 2 +- backend/internal/app/transaction/service.go | 2 -- backend/internal/app/user/handler.go | 13 +++++-- backend/internal/app/user/service.go | 12 ++----- backend/internal/repository/contribution.go | 13 ++----- backend/internal/repository/repository.go | 38 ++++---------------- backend/internal/repository/user.go | 12 +++---- 14 files changed, 85 insertions(+), 96 deletions(-) diff --git a/backend/internal/app/auth/handler.go b/backend/internal/app/auth/handler.go index 2be51640..3d40c859 100644 --- a/backend/internal/app/auth/handler.go +++ b/backend/internal/app/auth/handler.go @@ -7,12 +7,13 @@ import ( "github.com/joshsoftware/code-curiosity-2025/internal/config" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/middleware" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/response" ) type handler struct { authService Service - appConfig config.AppConfig + appConfig config.AppConfig } type Handler interface { @@ -24,7 +25,7 @@ type Handler interface { func NewHandler(authService Service, appConfig config.AppConfig) Handler { return &handler{ authService: authService, - appConfig: appConfig, + appConfig: appConfig, } } @@ -62,7 +63,16 @@ func (h *handler) GithubOAuthLoginCallback(w http.ResponseWriter, r *http.Reques func (h *handler) GetLoggedInUser(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - userInfo, err := h.authService.GetLoggedInUser(ctx) + userIdValue := ctx.Value(middleware.UserIdKey) + userId, ok := userIdValue.(int) + if !ok { + slog.Error("error obtaining user id from context") + status, errorMessage := apperrors.MapError(apperrors.ErrContextValue) + response.WriteJson(w, status, errorMessage, nil) + return + } + + userInfo, err := h.authService.GetLoggedInUser(ctx, userId) if err != nil { slog.Error("error getting logged in user") status, errorMessage := apperrors.MapError(err) diff --git a/backend/internal/app/auth/service.go b/backend/internal/app/auth/service.go index c0cbeb4f..abcaf195 100644 --- a/backend/internal/app/auth/service.go +++ b/backend/internal/app/auth/service.go @@ -9,7 +9,6 @@ import ( "github.com/joshsoftware/code-curiosity-2025/internal/config" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/jwt" - "github.com/joshsoftware/code-curiosity-2025/internal/pkg/middleware" "golang.org/x/oauth2" "golang.org/x/oauth2/github" ) @@ -23,7 +22,7 @@ type service struct { type Service interface { GithubOAuthLoginUrl(ctx context.Context) string GithubOAuthLoginCallback(ctx context.Context, code string) (string, error) - GetLoggedInUser(ctx context.Context) (User, error) + GetLoggedInUser(ctx context.Context, userId int) (User, error) } func NewService(userService user.Service, appCfg config.AppConfig) Service { @@ -94,15 +93,7 @@ func (s *service) GithubOAuthLoginCallback(ctx context.Context, code string) (st return jwtToken, nil } -func (s *service) GetLoggedInUser(ctx context.Context) (User, error) { - userIdValue := ctx.Value(middleware.UserIdKey) - - userId, ok := userIdValue.(int) - if !ok { - slog.Error("error obtaining user id from context") - return User{}, apperrors.ErrInternalServer - } - +func (s *service) GetLoggedInUser(ctx context.Context, userId int) (User, error) { user, err := s.userService.GetUserById(ctx, userId) if err != nil { slog.Error("failed to get logged in user", "error", err) diff --git a/backend/internal/app/contribution/handler.go b/backend/internal/app/contribution/handler.go index 8d604ad6..49ea7bf3 100644 --- a/backend/internal/app/contribution/handler.go +++ b/backend/internal/app/contribution/handler.go @@ -28,7 +28,16 @@ func NewHandler(contributionService Service) Handler { func (h *handler) FetchUserContributions(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - userContributions, err := h.contributionService.FetchUserContributions(ctx) + userIdValue := ctx.Value(middleware.UserIdKey) + userId, ok := userIdValue.(int) + if !ok { + slog.Error("error obtaining user id from context") + status, errorMessage := apperrors.MapError(apperrors.ErrContextValue) + response.WriteJson(w, status, errorMessage, nil) + return + } + + userContributions, err := h.contributionService.FetchUserContributions(ctx, userId) if err != nil { slog.Error("error fetching user contributions", "error", err) status, errorMessage := apperrors.MapError(err) diff --git a/backend/internal/app/contribution/service.go b/backend/internal/app/contribution/service.go index 6a46b14c..9a7e85c0 100644 --- a/backend/internal/app/contribution/service.go +++ b/backend/internal/app/contribution/service.go @@ -64,7 +64,7 @@ type Service interface { CreateContribution(ctx context.Context, contributionType string, contributionDetails ContributionResponse, repositoryId int, userId int) (Contribution, error) HandleContributionCreation(ctx context.Context, repositoryID int, contribution ContributionResponse) (Contribution, error) GetContributionScoreDetailsByContributionType(ctx context.Context, contributionType string) (ContributionScore, error) - FetchUserContributions(ctx context.Context) ([]FetchUserContributionsResponse, error) + FetchUserContributions(ctx context.Context, userId int) ([]FetchUserContributionsResponse, error) GetContributionByGithubEventId(ctx context.Context, githubEventId string) (Contribution, error) ListMonthlyContributionSummary(ctx context.Context, year int, monthParam int, userId int) ([]MonthlyContributionSummary, error) } @@ -205,7 +205,6 @@ func (s *service) GetContributionType(ctx context.Context, contribution Contribu } func (s *service) CreateContribution(ctx context.Context, contributionType string, contributionDetails ContributionResponse, repositoryId int, userId int) (Contribution, error) { - contribution := Contribution{ UserId: userId, RepositoryId: repositoryId, @@ -264,8 +263,8 @@ func (s *service) GetContributionScoreDetailsByContributionType(ctx context.Cont return ContributionScore(contributionScoreDetails), nil } -func (s *service) FetchUserContributions(ctx context.Context) ([]FetchUserContributionsResponse, error) { - userContributions, err := s.contributionRepository.FetchUserContributions(ctx, nil) +func (s *service) FetchUserContributions(ctx context.Context, userId int) ([]FetchUserContributionsResponse, error) { + userContributions, err := s.contributionRepository.FetchUserContributions(ctx, nil, userId) if err != nil { slog.Error("error occured while fetching user contributions", "error", err) return nil, err @@ -297,7 +296,6 @@ func (s *service) GetContributionByGithubEventId(ctx context.Context, githubEven } func (s *service) ListMonthlyContributionSummary(ctx context.Context, year int, month int, userId int) ([]MonthlyContributionSummary, error) { - MonthlyContributionSummaries, err := s.contributionRepository.ListMonthlyContributionSummary(ctx, nil, year, month, userId) if err != nil { slog.Error("error fetching monthly contribution summary", "error", err) diff --git a/backend/internal/app/goal/service.go b/backend/internal/app/goal/service.go index 34b38d1e..33a71f39 100644 --- a/backend/internal/app/goal/service.go +++ b/backend/internal/app/goal/service.go @@ -73,7 +73,7 @@ func (s *service) ListGoalLevelTargetDetail(ctx context.Context, userId int) ([] return serviceGoalLevelTargets, nil } -func (s *service) CreateCustomGoalLevelTarget(ctx context.Context, userID int, customGoalLevelTarget []CustomGoalLevelTarget) ([]GoalContribution, error) { +func (s *service) CreateCustomGoalLevelTarget(ctx context.Context, userId int, customGoalLevelTarget []CustomGoalLevelTarget) ([]GoalContribution, error) { customGoalLevelId, err := s.GetGoalIdByGoalLevel(ctx, "Custom") if err != nil { slog.Error("error fetching custom goal level id", "error", err) @@ -93,7 +93,7 @@ func (s *service) CreateCustomGoalLevelTarget(ctx context.Context, userID int, c goalContributionInfo[i].ContributionScoreId = contributionScoreDetails.Id goalContributionInfo[i].TargetCount = c.Target - goalContributionInfo[i].SetByUserId = userID + goalContributionInfo[i].SetByUserId = userId goalContribution, err := s.goalRepository.CreateCustomGoalLevelTarget(ctx, nil, repository.GoalContribution(goalContributionInfo[i])) if err != nil { diff --git a/backend/internal/app/repository/handler.go b/backend/internal/app/repository/handler.go index d2e76e66..58606146 100644 --- a/backend/internal/app/repository/handler.go +++ b/backend/internal/app/repository/handler.go @@ -7,6 +7,7 @@ import ( "github.com/joshsoftware/code-curiosity-2025/internal/app/github" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/middleware" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/response" ) @@ -35,7 +36,16 @@ func (h *handler) FetchUsersContributedRepos(w http.ResponseWriter, r *http.Requ client := &http.Client{} - usersContributedRepos, err := h.repositoryService.FetchUsersContributedRepos(ctx, client) + userIdValue := ctx.Value(middleware.UserIdKey) + userId, ok := userIdValue.(int) + if !ok { + slog.Error("error obtaining user id from context") + status, errorMessage := apperrors.MapError(apperrors.ErrContextValue) + response.WriteJson(w, status, errorMessage, nil) + return + } + + usersContributedRepos, err := h.repositoryService.FetchUsersContributedRepos(ctx, client, userId) if err != nil { slog.Error("error fetching users conributed repos", "error", err) status, errorMessage := apperrors.MapError(err) @@ -102,6 +112,15 @@ func (h *handler) FetchParticularRepoContributors(w http.ResponseWriter, r *http func (h *handler) FetchUserContributionsInRepo(w http.ResponseWriter, r *http.Request) { ctx := r.Context() + userIdValue := ctx.Value(middleware.UserIdKey) + userId, ok := userIdValue.(int) + if !ok { + slog.Error("error obtaining user id from context") + status, errorMessage := apperrors.MapError(apperrors.ErrContextValue) + response.WriteJson(w, status, errorMessage, nil) + return + } + repoIdPath := r.PathValue("repo_id") repoId, err := strconv.Atoi(repoIdPath) if err != nil { @@ -111,7 +130,7 @@ func (h *handler) FetchUserContributionsInRepo(w http.ResponseWriter, r *http.Re return } - usersContributionsInRepo, err := h.repositoryService.FetchUserContributionsInRepo(ctx, repoId) + usersContributionsInRepo, err := h.repositoryService.FetchUserContributionsInRepo(ctx, userId, repoId) if err != nil { slog.Error("error fetching users contribution in repository", "error", err) status, errorMessage := apperrors.MapError(err) diff --git a/backend/internal/app/repository/service.go b/backend/internal/app/repository/service.go index 3aa127cf..9f2afc31 100644 --- a/backend/internal/app/repository/service.go +++ b/backend/internal/app/repository/service.go @@ -21,9 +21,9 @@ type Service interface { GetRepoByRepoId(ctx context.Context, repoId int) (Repository, error) CreateRepository(ctx context.Context, repoGithubId int, ContributionRepoDetailsUrl string) (Repository, error) HandleRepositoryCreation(ctx context.Context, contribution ContributionResponse) (Repository, error) - FetchUsersContributedRepos(ctx context.Context, client *http.Client) ([]FetchUsersContributedReposResponse, error) + FetchUsersContributedRepos(ctx context.Context, client *http.Client, userId int) ([]FetchUsersContributedReposResponse, error) FetchParticularRepoDetails(ctx context.Context, repoId int) (FetchParticularRepoDetailsResponse, error) - FetchUserContributionsInRepo(ctx context.Context, githubRepoId int) ([]Contribution, error) + FetchUserContributionsInRepo(ctx context.Context, userId int, githubRepoId int) ([]Contribution, error) CalculateLanguagePercentInRepo(ctx context.Context, repoLanguages RepoLanguages) ([]LanguagePercent, error) FetchUserContributedReposCount(ctx context.Context, userId int) (int, error) } @@ -99,8 +99,8 @@ func (s *service) HandleRepositoryCreation(ctx context.Context, contribution Con return obtainedRepository, nil } -func (s *service) FetchUsersContributedRepos(ctx context.Context, client *http.Client) ([]FetchUsersContributedReposResponse, error) { - usersContributedRepos, err := s.repositoryRepository.FetchUsersContributedRepos(ctx, nil) +func (s *service) FetchUsersContributedRepos(ctx context.Context, client *http.Client, userId int) ([]FetchUsersContributedReposResponse, error) { + usersContributedRepos, err := s.repositoryRepository.FetchUsersContributedRepos(ctx, nil, userId) if err != nil { slog.Error("error fetching users conributed repos", "error", err) return nil, err @@ -121,7 +121,7 @@ func (s *service) FetchUsersContributedRepos(ctx context.Context, client *http.C fetchUsersContributedReposResponse[i].Languages = append(fetchUsersContributedReposResponse[i].Languages, language) } - userRepoTotalCoins, err := s.repositoryRepository.GetUserRepoTotalCoins(ctx, nil, usersContributedRepo.Id) + userRepoTotalCoins, err := s.repositoryRepository.GetUserRepoTotalCoins(ctx, nil, userId, usersContributedRepo.Id) if err != nil { slog.Error("error calculating total coins earned by user for the repository", "error", err) return nil, err @@ -159,8 +159,8 @@ func (s *service) FetchParticularRepoDetails(ctx context.Context, repoId int) (F return particularRepoDetails, nil } -func (s *service) FetchUserContributionsInRepo(ctx context.Context, githubRepoId int) ([]Contribution, error) { - userContributionsInRepo, err := s.repositoryRepository.FetchUserContributionsInRepo(ctx, nil, githubRepoId) +func (s *service) FetchUserContributionsInRepo(ctx context.Context, userId int, githubRepoId int) ([]Contribution, error) { + userContributionsInRepo, err := s.repositoryRepository.FetchUserContributionsInRepo(ctx, nil, userId, githubRepoId) if err != nil { slog.Error("error fetching users contribution in repository", "error", err) return nil, err diff --git a/backend/internal/app/router.go b/backend/internal/app/router.go index 818d2cef..13ce566a 100644 --- a/backend/internal/app/router.go +++ b/backend/internal/app/router.go @@ -20,7 +20,6 @@ func NewRouter(deps Dependencies) http.Handler { router.HandleFunc("PATCH /api/v1/user/email", middleware.Authentication(deps.UserHandler.UpdateUserEmail, deps.AppCfg)) router.HandleFunc("DELETE /api/v1/user/delete/{user_id}", middleware.Authentication(deps.UserHandler.SoftDeleteUser, deps.AppCfg)) - router.HandleFunc("PATCH /api/v1/user/goal/level", middleware.Authentication(deps.UserHandler.UpdateCurrentActiveGoalId, deps.AppCfg)) router.HandleFunc("GET /api/v1/user/contributions/all", middleware.Authentication(deps.ContributionHandler.FetchUserContributions, deps.AppCfg)) router.HandleFunc("GET /api/v1/user/overview", middleware.Authentication(deps.ContributionHandler.ListMonthlyContributionSummary, deps.AppCfg)) @@ -35,6 +34,7 @@ func NewRouter(deps Dependencies) http.Handler { router.HandleFunc("GET /api/v1/user/leaderboard", middleware.Authentication(deps.UserHandler.GetCurrentUserRank, deps.AppCfg)) router.HandleFunc("GET /api/v1/user/goal/level", middleware.Authentication(deps.GoalHandler.ListGoalLevels, deps.AppCfg)) + router.HandleFunc("PATCH /api/v1/user/goal/level", middleware.Authentication(deps.UserHandler.UpdateCurrentActiveGoalId, deps.AppCfg)) router.HandleFunc("GET /api/v1/user/goal/level/targets", middleware.Authentication(deps.GoalHandler.ListGoalLevelTargets, deps.AppCfg)) router.HandleFunc("POST /api/v1/user/goal/level/custom/targets", middleware.Authentication(deps.GoalHandler.CreateCustomGoalLevelTarget, deps.AppCfg)) router.HandleFunc("GET /api/v1/user/goal/level/targets/achieved", middleware.Authentication(deps.GoalHandler.ListGoalLevelAchievedTarget, deps.AppCfg)) diff --git a/backend/internal/app/transaction/service.go b/backend/internal/app/transaction/service.go index 59b4f177..3fa01470 100644 --- a/backend/internal/app/transaction/service.go +++ b/backend/internal/app/transaction/service.go @@ -89,8 +89,6 @@ func (s *service) CreateTransactionForContribution(ctx context.Context, contribu } func (s *service) HandleTransactionCreation(ctx context.Context, contribution Contribution) (Transaction, error) { - var transaction Transaction - transaction, err := s.GetTransactionByContributionId(ctx, contribution.Id) if err != nil { if err == apperrors.ErrTransactionNotFound { diff --git a/backend/internal/app/user/handler.go b/backend/internal/app/user/handler.go index 2d1bfcdc..8133c9f9 100644 --- a/backend/internal/app/user/handler.go +++ b/backend/internal/app/user/handler.go @@ -31,6 +31,15 @@ func NewHandler(userService Service) Handler { func (h *handler) UpdateUserEmail(w http.ResponseWriter, r *http.Request) { ctx := r.Context() + userIdValue := ctx.Value(middleware.UserIdKey) + userId, ok := userIdValue.(int) + if !ok { + slog.Error("error obtaining user id from context") + status, errorMessage := apperrors.MapError(apperrors.ErrContextValue) + response.WriteJson(w, status, errorMessage, nil) + return + } + var requestBody Email err := json.NewDecoder(r.Body).Decode(&requestBody) if err != nil { @@ -39,7 +48,7 @@ func (h *handler) UpdateUserEmail(w http.ResponseWriter, r *http.Request) { return } - err = h.userService.UpdateUserEmail(ctx, requestBody.Email) + err = h.userService.UpdateUserEmail(ctx, userId, requestBody.Email) if err != nil { slog.Error("failed to update user email", "error", err) status, errorMessage := apperrors.MapError(err) @@ -54,7 +63,6 @@ func (h *handler) SoftDeleteUser(w http.ResponseWriter, r *http.Request) { ctx := r.Context() userIdValue := ctx.Value(middleware.UserIdKey) - userId, ok := userIdValue.(int) if !ok { slog.Error("error obtaining user id from context") @@ -92,7 +100,6 @@ func (h *handler) GetCurrentUserRank(w http.ResponseWriter, r *http.Request) { ctx := r.Context() userIdValue := ctx.Value(middleware.UserIdKey) - userId, ok := userIdValue.(int) if !ok { slog.Error("error obtaining user id from context") diff --git a/backend/internal/app/user/service.go b/backend/internal/app/user/service.go index 339d6c95..2e58ba79 100644 --- a/backend/internal/app/user/service.go +++ b/backend/internal/app/user/service.go @@ -22,7 +22,7 @@ type Service interface { GetUserById(ctx context.Context, userId int) (User, error) GetUserByGithubId(ctx context.Context, githubId int) (User, error) CreateUser(ctx context.Context, userInfo CreateUserRequestBody) (User, error) - UpdateUserEmail(ctx context.Context, email string) error + UpdateUserEmail(ctx context.Context, userId int, email string) error SoftDeleteUser(ctx context.Context, userId int) error HardDeleteUsers(ctx context.Context) error RecoverAccountInGracePeriod(ctx context.Context, userID int) error @@ -71,15 +71,7 @@ func (s *service) CreateUser(ctx context.Context, userInfo CreateUserRequestBody return User(user), nil } -func (s *service) UpdateUserEmail(ctx context.Context, email string) error { - userIdValue := ctx.Value(middleware.UserIdKey) - - userId, ok := userIdValue.(int) - if !ok { - slog.Error("error obtaining user id from context") - return apperrors.ErrInternalServer - } - +func (s *service) UpdateUserEmail(ctx context.Context, userId int, email string) error { err := s.userRepository.UpdateUserEmail(ctx, nil, userId, email) if err != nil { slog.Error("failed to update user email", "error", err) diff --git a/backend/internal/repository/contribution.go b/backend/internal/repository/contribution.go index 7febf7fd..d3618464 100644 --- a/backend/internal/repository/contribution.go +++ b/backend/internal/repository/contribution.go @@ -8,7 +8,6 @@ import ( "github.com/jmoiron/sqlx" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" - "github.com/joshsoftware/code-curiosity-2025/internal/pkg/middleware" ) type contributionRepository struct { @@ -19,7 +18,7 @@ type ContributionRepository interface { RepositoryTransaction CreateContribution(ctx context.Context, tx *sqlx.Tx, contributionDetails Contribution) (Contribution, error) GetContributionScoreDetailsByContributionType(ctx context.Context, tx *sqlx.Tx, contributionType string) (ContributionScore, error) - FetchUserContributions(ctx context.Context, tx *sqlx.Tx) ([]Contribution, error) + FetchUserContributions(ctx context.Context, tx *sqlx.Tx, userId int) ([]Contribution, error) GetContributionByGithubEventId(ctx context.Context, tx *sqlx.Tx, githubEventId string) (Contribution, error) GetAllContributionTypes(ctx context.Context, tx *sqlx.Tx) ([]ContributionScore, error) ListMonthlyContributionSummary(ctx context.Context, tx *sqlx.Tx, year int, month int, userId int) ([]MonthlyContributionSummary, error) @@ -108,15 +107,7 @@ func (cr *contributionRepository) GetContributionScoreDetailsByContributionType( return contributionScoreDetails, nil } -func (cr *contributionRepository) FetchUserContributions(ctx context.Context, tx *sqlx.Tx) ([]Contribution, error) { - userIdValue := ctx.Value(middleware.UserIdKey) - - userId, ok := userIdValue.(int) - if !ok { - slog.Error("error obtaining user id from context") - return nil, apperrors.ErrInternalServer - } - +func (cr *contributionRepository) FetchUserContributions(ctx context.Context, tx *sqlx.Tx, userId int) ([]Contribution, error) { executer := cr.BaseRepository.initiateQueryExecuter(tx) var userContributions []Contribution diff --git a/backend/internal/repository/repository.go b/backend/internal/repository/repository.go index 97079d1f..4b2823c9 100644 --- a/backend/internal/repository/repository.go +++ b/backend/internal/repository/repository.go @@ -8,7 +8,6 @@ import ( "github.com/jmoiron/sqlx" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" - "github.com/joshsoftware/code-curiosity-2025/internal/pkg/middleware" ) type repositoryRepository struct { @@ -20,9 +19,9 @@ type RepositoryRepository interface { GetRepoByGithubId(ctx context.Context, tx *sqlx.Tx, repoGithubId int) (Repository, error) GetRepoByRepoId(ctx context.Context, tx *sqlx.Tx, repoId int) (Repository, error) CreateRepository(ctx context.Context, tx *sqlx.Tx, repository Repository) (Repository, error) - GetUserRepoTotalCoins(ctx context.Context, tx *sqlx.Tx, repoId int) (int, error) - FetchUsersContributedRepos(ctx context.Context, tx *sqlx.Tx) ([]Repository, error) - FetchUserContributionsInRepo(ctx context.Context, tx *sqlx.Tx, repoGithubId int) ([]Contribution, error) + GetUserRepoTotalCoins(ctx context.Context, tx *sqlx.Tx, userId int, repoId int) (int, error) + FetchUsersContributedRepos(ctx context.Context, tx *sqlx.Tx, userId int) ([]Repository, error) + FetchUserContributionsInRepo(ctx context.Context, tx *sqlx.Tx, userId int, repoGithubId int) ([]Contribution, error) FetchUserContributedReposCount(ctx context.Context, tx *sqlx.Tx, userId int) (int, error) } @@ -118,19 +117,10 @@ func (rr *repositoryRepository) CreateRepository(ctx context.Context, tx *sqlx.T } -func (r *repositoryRepository) GetUserRepoTotalCoins(ctx context.Context, tx *sqlx.Tx, repoId int) (int, error) { - userIdValue := ctx.Value(middleware.UserIdKey) - - userId, ok := userIdValue.(int) - if !ok { - slog.Error("error obtaining user id from context") - return 0, apperrors.ErrInternalServer - } - +func (r *repositoryRepository) GetUserRepoTotalCoins(ctx context.Context, tx *sqlx.Tx, userId int, repoId int) (int, error) { executer := r.BaseRepository.initiateQueryExecuter(tx) var totalCoins int - err := executer.GetContext(ctx, &totalCoins, getUserRepoTotalCoinsQuery, userId, repoId) if err != nil { slog.Error("error calculating total coins earned by user for the repository", "error", err) @@ -140,15 +130,7 @@ func (r *repositoryRepository) GetUserRepoTotalCoins(ctx context.Context, tx *sq return totalCoins, nil } -func (r *repositoryRepository) FetchUsersContributedRepos(ctx context.Context, tx *sqlx.Tx) ([]Repository, error) { - userIdValue := ctx.Value(middleware.UserIdKey) - - userId, ok := userIdValue.(int) - if !ok { - slog.Error("error obtaining user id from context") - return nil, apperrors.ErrInternalServer - } - +func (r *repositoryRepository) FetchUsersContributedRepos(ctx context.Context, tx *sqlx.Tx, userId int) ([]Repository, error) { executer := r.BaseRepository.initiateQueryExecuter(tx) var usersContributedRepos []Repository @@ -161,15 +143,7 @@ func (r *repositoryRepository) FetchUsersContributedRepos(ctx context.Context, t return usersContributedRepos, nil } -func (r *repositoryRepository) FetchUserContributionsInRepo(ctx context.Context, tx *sqlx.Tx, repoGithubId int) ([]Contribution, error) { - userIdValue := ctx.Value(middleware.UserIdKey) - - userId, ok := userIdValue.(int) - if !ok { - slog.Error("error obtaining user id from context") - return nil, apperrors.ErrInternalServer - } - +func (r *repositoryRepository) FetchUserContributionsInRepo(ctx context.Context, tx *sqlx.Tx, userId int, repoGithubId int) ([]Contribution, error) { executer := r.BaseRepository.initiateQueryExecuter(tx) var userContributionsInRepo []Contribution diff --git a/backend/internal/repository/user.go b/backend/internal/repository/user.go index 49f1a0bc..e75db674 100644 --- a/backend/internal/repository/user.go +++ b/backend/internal/repository/user.go @@ -21,8 +21,8 @@ type UserRepository interface { GetUserByGithubId(ctx context.Context, tx *sqlx.Tx, githubId int) (User, error) CreateUser(ctx context.Context, tx *sqlx.Tx, userInfo CreateUserRequestBody) (User, error) UpdateUserEmail(ctx context.Context, tx *sqlx.Tx, userId int, email string) error - MarkUserAsDeleted(ctx context.Context, tx *sqlx.Tx, userID int, deletedAt time.Time) error - RecoverAccountInGracePeriod(ctx context.Context, tx *sqlx.Tx, userID int) error + MarkUserAsDeleted(ctx context.Context, tx *sqlx.Tx, userId int, deletedAt time.Time) error + RecoverAccountInGracePeriod(ctx context.Context, tx *sqlx.Tx, userId int) error HardDeleteUsers(ctx context.Context, tx *sqlx.Tx) error GetAllUsersGithubId(ctx context.Context, tx *sqlx.Tx) ([]int, error) UpdateUserCurrentBalance(ctx context.Context, tx *sqlx.Tx, user User) error @@ -159,10 +159,10 @@ func (ur *userRepository) UpdateUserEmail(ctx context.Context, tx *sqlx.Tx, user return nil } -func (ur *userRepository) MarkUserAsDeleted(ctx context.Context, tx *sqlx.Tx, userID int, deletedAt time.Time) error { +func (ur *userRepository) MarkUserAsDeleted(ctx context.Context, tx *sqlx.Tx, userId int, deletedAt time.Time) error { executer := ur.BaseRepository.initiateQueryExecuter(tx) - _, err := executer.ExecContext(ctx, markUserAsDeletedQuery, deletedAt, userID) + _, err := executer.ExecContext(ctx, markUserAsDeletedQuery, deletedAt, userId) if err != nil { slog.Error("unable to mark user as deleted", "error", err) return apperrors.ErrInternalServer @@ -171,10 +171,10 @@ func (ur *userRepository) MarkUserAsDeleted(ctx context.Context, tx *sqlx.Tx, us return nil } -func (ur *userRepository) RecoverAccountInGracePeriod(ctx context.Context, tx *sqlx.Tx, userID int) error { +func (ur *userRepository) RecoverAccountInGracePeriod(ctx context.Context, tx *sqlx.Tx, userId int) error { executer := ur.BaseRepository.initiateQueryExecuter(tx) - _, err := executer.ExecContext(ctx, recoverAccountInGracePeriodQuery, userID) + _, err := executer.ExecContext(ctx, recoverAccountInGracePeriodQuery, userId) if err != nil { slog.Error("unable to reverse the soft delete ", "error", err) return apperrors.ErrInternalServer From a83e5fafb1f13d6aac952a5ebcb84b0a726d3df1 Mon Sep 17 00:00:00 2001 From: VishakhaSainani-Josh Date: Tue, 29 Jul 2025 17:36:55 +0530 Subject: [PATCH 12/36] refactor fetch repo cotributors response json format --- backend/internal/app/github/domain.go | 10 +++++++++- backend/internal/app/github/service.go | 15 ++++++++++----- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/backend/internal/app/github/domain.go b/backend/internal/app/github/domain.go index e4ec6002..1bc0916b 100644 --- a/backend/internal/app/github/domain.go +++ b/backend/internal/app/github/domain.go @@ -21,10 +21,18 @@ type FetchRepositoryDetailsResponse struct { type RepoLanguages map[string]int -type FetchRepoContributorsResponse struct { +type RepoContributorsResponse struct { Id int `json:"id"` Name string `json:"login"` AvatarUrl string `json:"avatar_url"` GithubUrl string `json:"html_url"` Contributions int `json:"contributions"` } + +type FetchRepositoryContributorsResponse struct { + Id int `json:"id"` + Name string `json:"name"` + AvatarUrl string `json:"avatarUrl"` + GithubUrl string `json:"githubUrl"` + Contributions int `json:"contributions"` +} diff --git a/backend/internal/app/github/service.go b/backend/internal/app/github/service.go index 16637c03..1ec62eff 100644 --- a/backend/internal/app/github/service.go +++ b/backend/internal/app/github/service.go @@ -19,7 +19,7 @@ type Service interface { configureGithubApiHeaders() map[string]string FetchRepositoryDetails(ctx context.Context, getUserRepoDetailsUrl string) (FetchRepositoryDetailsResponse, error) FetchRepositoryLanguages(ctx context.Context, getRepoLanguagesURL string) (RepoLanguages, error) - FetchRepositoryContributors(ctx context.Context, getRepoContributorsURl string) ([]FetchRepoContributorsResponse, error) + FetchRepositoryContributors(ctx context.Context, getRepoContributorsURl string) ([]FetchRepositoryContributorsResponse, error) } func NewService(appCfg config.AppConfig, httpClient *http.Client) Service { @@ -73,21 +73,26 @@ func (s *service) FetchRepositoryLanguages(ctx context.Context, getRepoLanguages return repoLanguages, nil } -func (s *service) FetchRepositoryContributors(ctx context.Context, getRepoContributorsURl string) ([]FetchRepoContributorsResponse, error) { +func (s *service) FetchRepositoryContributors(ctx context.Context, getRepoContributorsURl string) ([]FetchRepositoryContributorsResponse, error) { headers := s.configureGithubApiHeaders() body, err := utils.DoGet(s.httpClient, getRepoContributorsURl, headers) if err != nil { slog.Error("error making a GET request", "error", err) - return []FetchRepoContributorsResponse{}, err + return nil, err } - var repoContributors []FetchRepoContributorsResponse + var repoContributors []RepoContributorsResponse err = json.Unmarshal(body, &repoContributors) if err != nil { slog.Error("error unmarshalling fetch contributors body", "error", err) return nil, err } - return repoContributors, nil + serviceRepoContributors := make([]FetchRepositoryContributorsResponse, len(repoContributors)) + for i, c := range repoContributors { + serviceRepoContributors[i] = FetchRepositoryContributorsResponse(c) + } + + return serviceRepoContributors, nil } From fdd5ea0ff74d968b2390a1d1ef964e32c76dad44 Mon Sep 17 00:00:00 2001 From: VishakhaSainani-Josh Date: Tue, 29 Jul 2025 18:48:49 +0530 Subject: [PATCH 13/36] implement my-contirbutions page with repository details --- frontend/package.json | 2 + frontend/src/api/queries/Contributors.ts | 25 +++ frontend/src/api/queries/Languages.ts | 24 +++ frontend/src/api/queries/Repositories.ts | 22 +++ frontend/src/api/queries/Repository.tsx | 24 +++ .../src/api/queries/RepostoryActivities.ts | 25 +++ frontend/src/api/queries/UserBadges.ts | 22 +-- .../Login/components/LoginComponent.tsx | 5 +- frontend/src/features/Login/index.tsx | 7 +- .../components/Repositories.tsx | 29 ++++ .../components/RepositoriesCard.tsx | 65 ++++++++ .../src/features/MyContributions/index.tsx | 4 +- .../components/Contributors.tsx | 63 ++++++++ .../components/ContributorsCard.tsx | 39 +++++ .../components/Languages.tsx | 26 ++++ .../components/LanguagesCard.tsx | 61 ++++++++ .../components/Repository.tsx | 26 ++++ .../components/RepositoryActivities.tsx | 91 +++++++++++ .../components/RepositoryCard.tsx | 60 +++++++ .../features/RepositoryDetails.tsx/index.tsx | 37 +++++ .../components/RecentActivities.tsx | 1 + frontend/src/features/UserDashboard/index.tsx | 7 +- frontend/src/root/Router.tsx | 15 +- frontend/src/root/routes-config.tsx | 20 ++- .../shared/components/common/ActivityCard.tsx | 24 +-- frontend/src/shared/components/ui/avatar.tsx | 51 ++++++ frontend/src/shared/components/ui/dialog.tsx | 141 +++++++++++++++++ frontend/src/shared/constants/constants.ts | 25 +++ frontend/src/shared/constants/layout.ts | 8 + frontend/src/shared/constants/query-keys.ts | 7 +- frontend/src/shared/constants/routes.ts | 1 + frontend/src/shared/layout/AuthLayout.tsx | 8 +- frontend/src/shared/types/navbar.ts | 7 +- frontend/src/shared/types/types.ts | 146 ++++++++++++------ 34 files changed, 1025 insertions(+), 93 deletions(-) create mode 100644 frontend/src/api/queries/Contributors.ts create mode 100644 frontend/src/api/queries/Languages.ts create mode 100644 frontend/src/api/queries/Repositories.ts create mode 100644 frontend/src/api/queries/Repository.tsx create mode 100644 frontend/src/api/queries/RepostoryActivities.ts create mode 100644 frontend/src/features/MyContributions/components/Repositories.tsx create mode 100644 frontend/src/features/MyContributions/components/RepositoriesCard.tsx create mode 100644 frontend/src/features/RepositoryDetails.tsx/components/Contributors.tsx create mode 100644 frontend/src/features/RepositoryDetails.tsx/components/ContributorsCard.tsx create mode 100644 frontend/src/features/RepositoryDetails.tsx/components/Languages.tsx create mode 100644 frontend/src/features/RepositoryDetails.tsx/components/LanguagesCard.tsx create mode 100644 frontend/src/features/RepositoryDetails.tsx/components/Repository.tsx create mode 100644 frontend/src/features/RepositoryDetails.tsx/components/RepositoryActivities.tsx create mode 100644 frontend/src/features/RepositoryDetails.tsx/components/RepositoryCard.tsx create mode 100644 frontend/src/features/RepositoryDetails.tsx/index.tsx create mode 100644 frontend/src/shared/components/ui/avatar.tsx create mode 100644 frontend/src/shared/components/ui/dialog.tsx create mode 100644 frontend/src/shared/constants/constants.ts create mode 100644 frontend/src/shared/constants/layout.ts diff --git a/frontend/package.json b/frontend/package.json index f6ce37b5..465c6238 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,6 +14,8 @@ "format:fix": "npm run prettier -- --write" }, "dependencies": { + "@radix-ui/react-avatar": "^1.1.10", + "@radix-ui/react-dialog": "^1.1.14", "@radix-ui/react-dropdown-menu": "^2.1.15", "@radix-ui/react-progress": "^1.1.7", "@radix-ui/react-separator": "^1.1.7", diff --git a/frontend/src/api/queries/Contributors.ts b/frontend/src/api/queries/Contributors.ts new file mode 100644 index 00000000..76fa8589 --- /dev/null +++ b/frontend/src/api/queries/Contributors.ts @@ -0,0 +1,25 @@ +import type { ApiResponse } from "@/shared/types/api"; +import type { Contributor } from "@/shared/types/types"; +import { api } from "../axios"; +import { BACKEND_URL } from "@/shared/constants/endpoints"; +import { REPOSITORY_CONTRIBUTORS_QUERY_KEY } from "@/shared/constants/query-keys"; +import { useQuery } from "@tanstack/react-query"; + +const fetchRepositoryContributors = async ( + repoId: number +): Promise> => { + const response = await api.get<{ + message: string; + data: Contributor[]; + }>(`${BACKEND_URL}/api/v1/user/repositories/contributors/${repoId}`); + + return response.data; +}; + +export const useRepositoryContributors = (repoId: number) => { + return useQuery({ + queryKey: [REPOSITORY_CONTRIBUTORS_QUERY_KEY, repoId], + queryFn: () => fetchRepositoryContributors(repoId), + enabled: !!repoId + }); +}; diff --git a/frontend/src/api/queries/Languages.ts b/frontend/src/api/queries/Languages.ts new file mode 100644 index 00000000..3ab8ec6d --- /dev/null +++ b/frontend/src/api/queries/Languages.ts @@ -0,0 +1,24 @@ +import type { ApiResponse } from "@/shared/types/api"; +import type { Language } from "@/shared/types/types"; +import { api } from "../axios"; +import { BACKEND_URL } from "@/shared/constants/endpoints"; +import { REPOSITORY_LANGUAGES_QUERY_KEY } from "@/shared/constants/query-keys"; +import { useQuery } from "@tanstack/react-query"; + +const fetchRepositoryLanguages = async ( + id: number +): Promise> => { + const response = await api.get<{ + message: string; + data: Language[]; + }>(`${BACKEND_URL}/api/v1/user/repositories/languages/${id}`); + + return response.data; +}; + +export const useRepositoryLanguages = (id: number) => { + return useQuery({ + queryKey: [REPOSITORY_LANGUAGES_QUERY_KEY, id], + queryFn: () => fetchRepositoryLanguages(id) + }); +}; diff --git a/frontend/src/api/queries/Repositories.ts b/frontend/src/api/queries/Repositories.ts new file mode 100644 index 00000000..b0e70a4d --- /dev/null +++ b/frontend/src/api/queries/Repositories.ts @@ -0,0 +1,22 @@ +import type { Repositories } from "@/shared/types/types"; +import { api } from "../axios"; +import type { ApiResponse } from "@/shared/types/api"; +import { BACKEND_URL } from "@/shared/constants/endpoints"; +import { useQuery } from "@tanstack/react-query"; +import { REPOSITORIES_KEY } from "@/shared/constants/query-keys"; + +const fetchRepositories = async (): Promise> => { + const response = await api.get<{ + message: string; + data: Repositories[]; + }>(`${BACKEND_URL}/api/v1/user/repositories`); + + return response.data; +}; + +export const useRepositories = () => { + return useQuery({ + queryKey: [REPOSITORIES_KEY], + queryFn: fetchRepositories + }); +}; diff --git a/frontend/src/api/queries/Repository.tsx b/frontend/src/api/queries/Repository.tsx new file mode 100644 index 00000000..fa9434a4 --- /dev/null +++ b/frontend/src/api/queries/Repository.tsx @@ -0,0 +1,24 @@ +import { BACKEND_URL } from "@/shared/constants/endpoints"; +import type { ApiResponse } from "@/shared/types/api"; +import type { Repository } from "@/shared/types/types"; +import { api } from "../axios"; +import { REPOSITORY_KEY } from "@/shared/constants/query-keys"; +import { useQuery } from "@tanstack/react-query"; + +const fetchRepository = async ( + repoId: number +): Promise> => { + const response = await api.get<{ + message: string; + data: Repository; + }>(`${BACKEND_URL}/api/v1/user/repositories/${repoId}`); + + return response.data; +}; + +export const useRepository = (repoId: number) => { + return useQuery({ + queryKey: [REPOSITORY_KEY, repoId], + queryFn: () => fetchRepository(repoId) + }); +}; diff --git a/frontend/src/api/queries/RepostoryActivities.ts b/frontend/src/api/queries/RepostoryActivities.ts new file mode 100644 index 00000000..5610284c --- /dev/null +++ b/frontend/src/api/queries/RepostoryActivities.ts @@ -0,0 +1,25 @@ +import type { ApiResponse } from "@/shared/types/api"; +import type { RepositoryActivity } from "@/shared/types/types"; +import { api } from "../axios"; +import { BACKEND_URL } from "@/shared/constants/endpoints"; +import { useQuery } from "@tanstack/react-query"; +import { REPOSITORY_ACTIVITIES_QUERY_KEY } from "@/shared/constants/query-keys"; + +const fetchRepositoryActivivties = async ( + repoId: number +): Promise> => { + const response = await api.get<{ + message: string; + data: RepositoryActivity[]; + }>(`${BACKEND_URL}/api/v1/user/repositories/contributions/recent/${repoId}`); + + return response.data; +}; + +export const useRepositoryActivities = (repoId: number) => { + return useQuery({ + queryKey: [REPOSITORY_ACTIVITIES_QUERY_KEY, repoId], + queryFn: () => fetchRepositoryActivivties(repoId), + enabled: !!repoId + }); +}; diff --git a/frontend/src/api/queries/UserBadges.ts b/frontend/src/api/queries/UserBadges.ts index 9f50e857..34271e22 100644 --- a/frontend/src/api/queries/UserBadges.ts +++ b/frontend/src/api/queries/UserBadges.ts @@ -6,17 +6,17 @@ import { USER_BADGES_QUERY_KEY } from "@/shared/constants/query-keys"; import { useQuery } from "@tanstack/react-query"; const fetchUserBadges = async (): Promise> => { - const response = await api.get<{ - message: string; - data: Badge[]; - }>(`${BACKEND_URL}/api/v1/user/badges`); + const response = await api.get<{ + message: string; + data: Badge[]; + }>(`${BACKEND_URL}/api/v1/user/badges`); - return response.data; -} + return response.data; +}; export const useUserBadges = () => { - return useQuery({ - queryKey: [USER_BADGES_QUERY_KEY], - queryFn: fetchUserBadges, - }); -} \ No newline at end of file + return useQuery({ + queryKey: [USER_BADGES_QUERY_KEY], + queryFn: fetchUserBadges + }); +}; diff --git a/frontend/src/features/Login/components/LoginComponent.tsx b/frontend/src/features/Login/components/LoginComponent.tsx index 9084984c..a76bf347 100644 --- a/frontend/src/features/Login/components/LoginComponent.tsx +++ b/frontend/src/features/Login/components/LoginComponent.tsx @@ -12,7 +12,10 @@ import { ACCESS_TOKEN_KEY } from "@/shared/constants/local-storage"; const LoginComponent = () => { const handleGithubLogin = () => { window.location.href = GITHUB_AUTH_URL || ""; - localStorage.setItem(ACCESS_TOKEN_KEY, 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjIsIklzQWRtaW4iOmZhbHNlLCJleHAiOjE3NTQxMzI3NTh9.VKEboNEvSeVKYnqLuBrvTyvx9IglhYzEyeE57x7Qzto') + localStorage.setItem( + ACCESS_TOKEN_KEY, + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjIsIklzQWRtaW4iOmZhbHNlLCJleHAiOjE3NTQxMzI3NTh9.VKEboNEvSeVKYnqLuBrvTyvx9IglhYzEyeE57x7Qzto" + ); }; return ( diff --git a/frontend/src/features/Login/index.tsx b/frontend/src/features/Login/index.tsx index 4114e41d..e254c720 100644 --- a/frontend/src/features/Login/index.tsx +++ b/frontend/src/features/Login/index.tsx @@ -1,12 +1,7 @@ -import AuthLayout from "@/shared/layout/AuthLayout"; import LoginComponent from "@/features/Login/components/LoginComponent"; const Login = () => { - return ( - - - - ); + return ; }; export default Login; diff --git a/frontend/src/features/MyContributions/components/Repositories.tsx b/frontend/src/features/MyContributions/components/Repositories.tsx new file mode 100644 index 00000000..b7fb3461 --- /dev/null +++ b/frontend/src/features/MyContributions/components/Repositories.tsx @@ -0,0 +1,29 @@ +import { useRepositories } from "@/api/queries/Repositories"; +import { Separator } from "@/shared/components/ui/separator"; +import RepositoriesCard from "./RepositoriesCard"; + +const Repositories = () => { + const { data } = useRepositories(); + const repositoriesData = data?.data; + + return ( +
+ {repositoriesData?.map(repo => ( + <> + + + + ))} +
+ ); +}; + +export default Repositories; diff --git a/frontend/src/features/MyContributions/components/RepositoriesCard.tsx b/frontend/src/features/MyContributions/components/RepositoriesCard.tsx new file mode 100644 index 00000000..266df629 --- /dev/null +++ b/frontend/src/features/MyContributions/components/RepositoriesCard.tsx @@ -0,0 +1,65 @@ +import Coin from "@/shared/components/common/Coin"; +import { Card, CardContent } from "@/shared/components/ui/card"; +import { LangColor } from "@/shared/constants/constants"; +import { format } from "date-fns"; +import type { FC } from "react"; +import { useNavigate } from "react-router-dom"; + +interface RepositoriesCardProps { + id: number; + name: string; + languages: string[]; + description: string; + updatedOn: string; + coins: number; +} + +const RepositoriesCard: FC = ({ + id, + name, + languages, + description, + updatedOn, + coins +}) => { + const navigate = useNavigate(); + + return ( + + +
navigate(`/repositories/${id}`)} + > +

{name}

+
+ + {coins} +
+
+ +
+ {languages?.map((language, index) => { + const color = LangColor[language] || "bg-gray-400"; + return ( +
+
+ {language} +
+ ); + })} +
+ +

+ {description || "No description for the given repository"} +

+ +

+ Updated on {format(new Date(updatedOn), "MMM d, yyyy")} +

+
+
+ ); +}; + +export default RepositoriesCard; diff --git a/frontend/src/features/MyContributions/index.tsx b/frontend/src/features/MyContributions/index.tsx index 3a454a7c..c2b31707 100644 --- a/frontend/src/features/MyContributions/index.tsx +++ b/frontend/src/features/MyContributions/index.tsx @@ -1,7 +1,7 @@ -import UserDashboardLayout from "@/shared/layout/UserDashboardLayout"; +import Repositories from "./components/Repositories"; const MyContributions = () => { - return ; + return ; }; export default MyContributions; diff --git a/frontend/src/features/RepositoryDetails.tsx/components/Contributors.tsx b/frontend/src/features/RepositoryDetails.tsx/components/Contributors.tsx new file mode 100644 index 00000000..75824937 --- /dev/null +++ b/frontend/src/features/RepositoryDetails.tsx/components/Contributors.tsx @@ -0,0 +1,63 @@ +import { useRepositoryContributors } from "@/api/queries/Contributors"; +import { useParams } from "react-router-dom"; +import ContributorsCard from "./ContributorsCard"; +import { useState } from "react"; +import { Button } from "@/shared/components/ui/button"; +import clsx from "clsx"; + +const ContributorsList = () => { + const [viewAll, setViewAll] = useState(false); + + const handleViewAll = () => { + setViewAll(!viewAll); + }; + + const { repoid } = useParams(); + const repoId = Number(repoid); + const { data, isLoading } = useRepositoryContributors(repoId); + const contributors = data?.data ?? []; + + const contributorsData = viewAll ? contributors : contributors?.slice(0, 20); + + return ( +
+
+

+ Contributors {contributors.length} +

+ +
+ {isLoading ? ( +
+
+
+ ) : ( +
+
+ {contributorsData?.map(contributor => ( + + ))} +
+
+ )} +
+ ); +}; + +export default ContributorsList; diff --git a/frontend/src/features/RepositoryDetails.tsx/components/ContributorsCard.tsx b/frontend/src/features/RepositoryDetails.tsx/components/ContributorsCard.tsx new file mode 100644 index 00000000..883224a0 --- /dev/null +++ b/frontend/src/features/RepositoryDetails.tsx/components/ContributorsCard.tsx @@ -0,0 +1,39 @@ +import { Avatar, AvatarFallback, AvatarImage } from "@radix-ui/react-avatar"; +import type { FC } from "react"; +import { Link } from "react-router-dom"; + +interface ContributorsCardProps { + name: string; + avatarUrl: string; + contributions: number; + githubUrl: string; +} + +const ContributorsCard: FC = ({ + name, + avatarUrl, + contributions, + githubUrl +}) => { + return ( +
+ + + + Contributors-Image +
+ {name}
+ {contributions} Contributions +
+
+ +
+
+ ); +}; + +export default ContributorsCard; diff --git a/frontend/src/features/RepositoryDetails.tsx/components/Languages.tsx b/frontend/src/features/RepositoryDetails.tsx/components/Languages.tsx new file mode 100644 index 00000000..3238ed4b --- /dev/null +++ b/frontend/src/features/RepositoryDetails.tsx/components/Languages.tsx @@ -0,0 +1,26 @@ +import { type FC } from "react"; +import LanguageCard from "../../RepositoryDetails.tsx/components/LanguagesCard"; +import { useRepositoryLanguages } from "@/api/queries/Languages"; +import { useParams } from "react-router-dom"; + +interface LanguagesProps { + className?: string; +} + +const Languages: FC = ({ className }) => { + const { repoid } = useParams(); + const repoId = Number(repoid); + const { data } = useRepositoryLanguages(repoId); + + const languagesData = data?.data; + return ( + + ); +}; + +export default Languages; +export type { LanguagesProps }; diff --git a/frontend/src/features/RepositoryDetails.tsx/components/LanguagesCard.tsx b/frontend/src/features/RepositoryDetails.tsx/components/LanguagesCard.tsx new file mode 100644 index 00000000..15d0fde2 --- /dev/null +++ b/frontend/src/features/RepositoryDetails.tsx/components/LanguagesCard.tsx @@ -0,0 +1,61 @@ +import { type FC } from "react"; +import clsx from "clsx"; +import type { Language } from "@/shared/types/types"; +import { LangColor } from "@/shared/constants/constants"; + +interface LanguageCardProps { + title?: string; + languages: Language[]; + className?: string; +} + +const LanguageCard: FC = ({ + title = "Languages", + languages, + className +}) => { + return ( +
+

{title}

+ +
+ {languages.map((language, index) => { + console.log(language.name); + const bgColor = LangColor[language.name] || "bg-gray-400"; + return ( +
+ ); + })} +
+ +
+ {languages.map((language, index) => { + const color = LangColor[language.name] || "bg-gray-400"; + return ( +
+
+ + {language.name} {language.percentage}% + +
+ ); + })} +
+
+ ); +}; + +export default LanguageCard; +export type { LanguageCardProps }; diff --git a/frontend/src/features/RepositoryDetails.tsx/components/Repository.tsx b/frontend/src/features/RepositoryDetails.tsx/components/Repository.tsx new file mode 100644 index 00000000..72e592fe --- /dev/null +++ b/frontend/src/features/RepositoryDetails.tsx/components/Repository.tsx @@ -0,0 +1,26 @@ +import RepositoryCard from "./RepositoryCard"; +import { useRepository } from "@/api/queries/Repository"; +import { useParams } from "react-router-dom"; + +const Repository = () => { + const { repoid } = useParams(); + const repoId = Number(repoid); + const { data } = useRepository(repoId); + const repo = data?.data; + + return ( +
+ +
+ ); +}; + +export default Repository; diff --git a/frontend/src/features/RepositoryDetails.tsx/components/RepositoryActivities.tsx b/frontend/src/features/RepositoryDetails.tsx/components/RepositoryActivities.tsx new file mode 100644 index 00000000..094c87f1 --- /dev/null +++ b/frontend/src/features/RepositoryDetails.tsx/components/RepositoryActivities.tsx @@ -0,0 +1,91 @@ +import { useState, type FC } from "react"; +import clsx from "clsx"; +import { Button } from "@/shared/components/ui/button"; +import { Card } from "@/shared/components/ui/card"; +import ActivityCard from "@/shared/components/common/ActivityCard"; +import { Link, useParams } from "react-router-dom"; +import { TrendingUp } from "lucide-react"; +import { useRepositoryActivities } from "@/api/queries/RepostoryActivities"; + +interface RepositoryActivitiesProps { + className?: string; +} + +const RepositoryActivities: FC = ({ className }) => { + const [viewAll, setViewAll] = useState(false); + + const handleViewAll = () => { + setViewAll(!viewAll); + }; + + const { repoid } = useParams(); + const repoId = Number(repoid); + const { data, isLoading } = useRepositoryActivities(repoId); + const repositoryActivities = data?.data ?? []; + const repositoryActivitiesData = viewAll + ? repositoryActivities + : repositoryActivities?.slice(0, 4); + + return ( + +
+

Recent Activities

+ +
+ + {isLoading ? ( +
+
+
+ ) : repositoryActivitiesData?.length === 0 ? ( +
+ +

+ No recent activities found +

+
+ ) : ( +
+ {repositoryActivitiesData?.map((activity, index) => ( + + ))} + {!viewAll && ( +
+ + How does points work? + +
+ )} +
+ )} +
+ ); +}; + +export default RepositoryActivities; diff --git a/frontend/src/features/RepositoryDetails.tsx/components/RepositoryCard.tsx b/frontend/src/features/RepositoryDetails.tsx/components/RepositoryCard.tsx new file mode 100644 index 00000000..ec53fac5 --- /dev/null +++ b/frontend/src/features/RepositoryDetails.tsx/components/RepositoryCard.tsx @@ -0,0 +1,60 @@ +import { LangColor } from "@/shared/constants/constants"; +import { ExternalLink } from "lucide-react"; +import type { FC } from "react"; + +interface RepositoriesCardProps { + name: string; + languages: string[]; + description: string; + updatedOn: string; + owner: string; + repoUrl: string; +} + +const RepositoryCard: FC = ({ + name, + languages, + description, + owner, + repoUrl +}) => { + return ( +
+
+
+ {name} + + + +
+

+ Owned By: {owner} +

+
+ +
+ {languages?.map((language, index) => { + const color = LangColor[language] || "bg-gray-400"; + return ( +
+
+ {language} +
+ ); + })} +
+ +

+ {description || + "No description for the given repository. Lorem ipsum dolor sit amet."} +

+
+ ); +}; + +export default RepositoryCard; diff --git a/frontend/src/features/RepositoryDetails.tsx/index.tsx b/frontend/src/features/RepositoryDetails.tsx/index.tsx new file mode 100644 index 00000000..953dadff --- /dev/null +++ b/frontend/src/features/RepositoryDetails.tsx/index.tsx @@ -0,0 +1,37 @@ +import Repository from "./components/Repository"; +import Languages from "./components/Languages"; +import RecentActivities from "./components/RepositoryActivities"; +import ContributorsList from "./components/Contributors"; +import { ArrowLeft } from "lucide-react"; +import { useNavigate } from "react-router-dom"; +import { Separator } from "@/shared/components/ui/separator"; + +const RepositoryDetails = () => { + const navigate = useNavigate(); + return ( +
+
+ + navigate("/my-contributions")} + > + Repository Details + +
+
+
+ + + +
+
+ + +
+
+
+ ); +}; + +export default RepositoryDetails; diff --git a/frontend/src/features/UserDashboard/components/RecentActivities.tsx b/frontend/src/features/UserDashboard/components/RecentActivities.tsx index 614807a0..5b016b4c 100644 --- a/frontend/src/features/UserDashboard/components/RecentActivities.tsx +++ b/frontend/src/features/UserDashboard/components/RecentActivities.tsx @@ -68,6 +68,7 @@ const RecentActivities: FC = ({ className }) => { contributedAt={activity.contributedAt} balanceChange={activity.balanceChange} showLine={index < recentActivitiesData.length - 1} + isRepositoryActivity={false} /> ))} {!viewAll && ( diff --git a/frontend/src/features/UserDashboard/index.tsx b/frontend/src/features/UserDashboard/index.tsx index dcc63321..554ee80e 100644 --- a/frontend/src/features/UserDashboard/index.tsx +++ b/frontend/src/features/UserDashboard/index.tsx @@ -1,12 +1,7 @@ -import UserDashboardLayout from "@/shared/layout/UserDashboardLayout"; import UserDashboardComponent from "@/features/UserDashboard/components/UserDashboardComponent"; const UserDashboard = () => { - return ( - - - - ); + return ; }; export default UserDashboard; diff --git a/frontend/src/root/Router.tsx b/frontend/src/root/Router.tsx index dc00eced..c6a6a359 100644 --- a/frontend/src/root/Router.tsx +++ b/frontend/src/root/Router.tsx @@ -2,15 +2,28 @@ import { RouterProvider, createBrowserRouter } from "react-router-dom"; import WithAuth from "@/shared/HOC/WithAuth"; import { type RoutesType, routesConfig } from "@/root/routes-config"; +import { Layout } from "@/shared/constants/layout"; +import AuthLayout from "@/shared/layout/AuthLayout"; +import UserDashboardLayout from "@/shared/layout/UserDashboardLayout"; const generateRoutes = (routes: RoutesType[]) => { - return routes.map(({ path, element, isProtected }) => { + return routes.map(({ path, element, isProtected, layout }) => { let wrappedElement = element; if (isProtected) { wrappedElement = {wrappedElement}; } + if (layout == Layout.AuthLayout) { + wrappedElement = {wrappedElement}; + } + + if (layout == Layout.DashboardLayout) { + wrappedElement = ( + {wrappedElement} + ); + } + return { path, element: wrappedElement }; }); }; diff --git a/frontend/src/root/routes-config.tsx b/frontend/src/root/routes-config.tsx index 62633f1f..4b98220d 100644 --- a/frontend/src/root/routes-config.tsx +++ b/frontend/src/root/routes-config.tsx @@ -1,34 +1,46 @@ import type { ReactNode } from "react"; - +import { Layout, type LayoutType } from "@/shared/constants/layout"; import Login from "@/features/Login"; import MyContributions from "@/features/MyContributions"; import UserDashboard from "@/features/UserDashboard"; import { LOGIN_PATH, MY_CONTRIBUTIONS_PATH, + REPOSITORY_DETAILS_PATH, USER_DASHBOARD_PATH } from "@/shared/constants/routes"; +import RepositoryDetails from "@/features/RepositoryDetails.tsx"; export interface RoutesType { path: string; element: ReactNode; isProtected?: boolean; + layout: LayoutType; } export const routesConfig: RoutesType[] = [ { path: LOGIN_PATH, element: , - isProtected: false + isProtected: false, + layout: Layout.AuthLayout }, { path: USER_DASHBOARD_PATH, element: , - isProtected: false + isProtected: false, + layout: Layout.DashboardLayout }, { path: MY_CONTRIBUTIONS_PATH, element: , - isProtected: false + isProtected: false, + layout: Layout.DashboardLayout + }, + { + path: REPOSITORY_DETAILS_PATH, + element: , + isProtected: false, + layout: Layout.DashboardLayout } ]; diff --git a/frontend/src/shared/components/common/ActivityCard.tsx b/frontend/src/shared/components/common/ActivityCard.tsx index 5d55a755..fb473e9e 100644 --- a/frontend/src/shared/components/common/ActivityCard.tsx +++ b/frontend/src/shared/components/common/ActivityCard.tsx @@ -1,12 +1,14 @@ import type { FC } from "react"; import Coin from "@/shared/components/common/Coin"; +import { format } from "date-fns"; interface ActivityCardProps { contributionType: string; - repositoryName: string; + repositoryName?: string; contributedAt: string; balanceChange: number; showLine: boolean; + isRepositoryActivity?: boolean; } const ActivityCard: FC = ({ @@ -14,7 +16,8 @@ const ActivityCard: FC = ({ repositoryName, contributedAt, balanceChange, - showLine = true + showLine = true, + isRepositoryActivity }) => { return (
@@ -29,14 +32,17 @@ const ActivityCard: FC = ({
{contributionType}
+ {isRepositoryActivity ? null : ( +
+ Contributed to + + <{repositoryName}> + +
+ )} +
- Contributed to{" "} - - <{repositoryName}> - -
-
- Contributed on {contributedAt} + Contributed on {format(new Date(contributedAt), "MMM d yyyy")}
diff --git a/frontend/src/shared/components/ui/avatar.tsx b/frontend/src/shared/components/ui/avatar.tsx new file mode 100644 index 00000000..834f26fd --- /dev/null +++ b/frontend/src/shared/components/ui/avatar.tsx @@ -0,0 +1,51 @@ +import * as React from "react" +import * as AvatarPrimitive from "@radix-ui/react-avatar" + +import { cn } from "@/shared/utils/tailwindcss" + +function Avatar({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AvatarImage({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AvatarFallback({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Avatar, AvatarImage, AvatarFallback } diff --git a/frontend/src/shared/components/ui/dialog.tsx b/frontend/src/shared/components/ui/dialog.tsx new file mode 100644 index 00000000..72339f36 --- /dev/null +++ b/frontend/src/shared/components/ui/dialog.tsx @@ -0,0 +1,141 @@ +import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { XIcon } from "lucide-react" + +import { cn } from "@/shared/utils/tailwindcss" + +function Dialog({ + ...props +}: React.ComponentProps) { + return +} + +function DialogTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function DialogPortal({ + ...props +}: React.ComponentProps) { + return +} + +function DialogClose({ + ...props +}: React.ComponentProps) { + return +} + +function DialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DialogContent({ + className, + children, + showCloseButton = true, + ...props +}: React.ComponentProps & { + showCloseButton?: boolean +}) { + return ( + + + + {children} + {showCloseButton && ( + + + Close + + )} + + + ) +} + +function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, +} diff --git a/frontend/src/shared/constants/constants.ts b/frontend/src/shared/constants/constants.ts new file mode 100644 index 00000000..862fd73e --- /dev/null +++ b/frontend/src/shared/constants/constants.ts @@ -0,0 +1,25 @@ +export const AuthLayoutDetails = [ + "Earn and Upskill", + "Set Your Goals", + "Leader Board", + "Open Source Contribution" +]; + +export const LangColor: Record = { + JavaScript: "bg-yellow-400", + TypeScript: "bg-blue-500", + Python: "bg-green-500", + Java: "bg-red-500", + Go: "bg-cyan-500", + Rust: "bg-orange-700", + C: "bg-gray-500", + "C++": "bg-purple-600", + Ruby: "bg-pink-500", + PHP: "bg-indigo-500", + Swift: "bg-orange-400", + Kotlin: "bg-violet-500", + Dart: "bg-sky-500", + HTML: "bg-orange-300", + CSS: "bg-blue-300", + Shell: "bg-zinc-600" +}; diff --git a/frontend/src/shared/constants/layout.ts b/frontend/src/shared/constants/layout.ts new file mode 100644 index 00000000..9be0dcda --- /dev/null +++ b/frontend/src/shared/constants/layout.ts @@ -0,0 +1,8 @@ +export const Layout = { + AuthLayout: "AuthLayout", + DashboardLayout: "DashboardLayout", + None: "None" +} as const; + +export type LayoutType = (typeof Layout)[keyof typeof Layout]; + diff --git a/frontend/src/shared/constants/query-keys.ts b/frontend/src/shared/constants/query-keys.ts index 5f5b57c5..f344fec4 100644 --- a/frontend/src/shared/constants/query-keys.ts +++ b/frontend/src/shared/constants/query-keys.ts @@ -3,4 +3,9 @@ export const USER_BADGES_QUERY_KEY = "user-badges" export const LEADERBOARD_QUERY_KEY="leaderboard" export const CURRENT_USER_RANK_QUERY_KEY="current-user-rank" export const RECENT_ACTIVITIES_QUERY_KEY="recent-activities" -export const OVERVIEW_QUERY_KEY="overview" \ No newline at end of file +export const OVERVIEW_QUERY_KEY="overview" +export const REPOSITORIES_KEY="repositories" +export const REPOSITORY_KEY="repository" +export const REPOSITORY_CONTRIBUTORS_QUERY_KEY = "repository-contributors" +export const REPOSITORY_LANGUAGES_QUERY_KEY = "repository-languages" +export const REPOSITORY_ACTIVITIES_QUERY_KEY="repository-activites" diff --git a/frontend/src/shared/constants/routes.ts b/frontend/src/shared/constants/routes.ts index 8cbdabfc..3294ebf7 100644 --- a/frontend/src/shared/constants/routes.ts +++ b/frontend/src/shared/constants/routes.ts @@ -2,3 +2,4 @@ export const LOGIN_PATH = "/login"; export const USER_DASHBOARD_PATH = "/"; export const MY_CONTRIBUTIONS_PATH = "/my-contributions"; +export const REPOSITORY_DETAILS_PATH = "/repositories/:repoid"; diff --git a/frontend/src/shared/layout/AuthLayout.tsx b/frontend/src/shared/layout/AuthLayout.tsx index 9cbad0b2..083ac3a4 100644 --- a/frontend/src/shared/layout/AuthLayout.tsx +++ b/frontend/src/shared/layout/AuthLayout.tsx @@ -5,6 +5,7 @@ import { CheckCircle } from "lucide-react"; import { Card } from "@/shared/components/ui/card"; import { LOGIN_PATH, USER_DASHBOARD_PATH } from "@/shared/constants/routes"; import { getAccessToken } from "@/shared/utils/local-storage"; +import { AuthLayoutDetails } from "../constants/constants"; interface AuthLayoutProps { children: ReactNode; @@ -45,12 +46,7 @@ const AuthLayout: FC = ({ children }) => {
- {[ - "Earn and Upskill", - "Set Your Goals", - "Leader Board", - "Open Source Contribution" - ].map((text, i) => ( + {AuthLayoutDetails.map((text, i) => (
Date: Mon, 4 Aug 2025 12:40:44 +0530 Subject: [PATCH 14/36] implement admin feature and refactor goal feature --- backend/internal/app/auth/domain.go | 9 +++ backend/internal/app/auth/service.go | 29 +++++++ backend/internal/app/contribution/domain.go | 5 ++ backend/internal/app/contribution/handler.go | 48 ++++++++++++ backend/internal/app/contribution/service.go | 37 +++++++++ backend/internal/app/goal/domain.go | 6 ++ backend/internal/app/goal/handler.go | 21 ++--- backend/internal/app/goal/service.go | 78 +++++++++---------- backend/internal/app/router.go | 9 ++- backend/internal/app/user/domain.go | 5 ++ backend/internal/app/user/service.go | 11 +++ backend/internal/pkg/apperrors/errors.go | 2 + backend/internal/pkg/middleware/middleware.go | 18 +++++ backend/internal/repository/contribution.go | 24 ++++++ backend/internal/repository/domain.go | 10 +++ backend/internal/repository/goal.go | 5 ++ backend/internal/repository/user.go | 20 +++++ 17 files changed, 283 insertions(+), 54 deletions(-) diff --git a/backend/internal/app/auth/domain.go b/backend/internal/app/auth/domain.go index 33f8f994..81687500 100644 --- a/backend/internal/app/auth/domain.go +++ b/backend/internal/app/auth/domain.go @@ -39,3 +39,12 @@ type GithubUserResponse struct { IsAdmin bool `json:"isAdmin"` } +type AdminLoginRequest struct { + Email string `json:"email"` + Password string `json:"password"` +} + +type Admin struct { + User + JwtToken string `json:"jwtToken"` +} diff --git a/backend/internal/app/auth/service.go b/backend/internal/app/auth/service.go index abcaf195..3e237d23 100644 --- a/backend/internal/app/auth/service.go +++ b/backend/internal/app/auth/service.go @@ -9,6 +9,7 @@ import ( "github.com/joshsoftware/code-curiosity-2025/internal/config" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/jwt" + "golang.org/x/crypto/bcrypt" "golang.org/x/oauth2" "golang.org/x/oauth2/github" ) @@ -23,6 +24,7 @@ type Service interface { GithubOAuthLoginUrl(ctx context.Context) string GithubOAuthLoginCallback(ctx context.Context, code string) (string, error) GetLoggedInUser(ctx context.Context, userId int) (User, error) + VerifyAdminCredentials(ctx context.Context, adminCredentials AdminLoginRequest) (Admin, error) } func NewService(userService user.Service, appCfg config.AppConfig) Service { @@ -102,3 +104,30 @@ func (s *service) GetLoggedInUser(ctx context.Context, userId int) (User, error) return User(user), nil } + +func (s *service) VerifyAdminCredentials(ctx context.Context, adminCredentials AdminLoginRequest) (Admin, error) { + adminInfo, err := s.userService.GetLoggedInAdmin(ctx, user.AdminLoginRequest(adminCredentials)) + if err != nil { + slog.Error("failed to verify admin", "error", err) + return Admin{}, err + } + + err = bcrypt.CompareHashAndPassword([]byte(adminInfo.Password), []byte(adminCredentials.Password)) + if err != nil { + slog.Error("failed to verify admin, invalid password", "error", err) + return Admin{}, apperrors.ErrInvalidCredentials + } + + jwtToken, err := jwt.GenerateJWT(adminInfo.Id, adminInfo.IsAdmin, s.appCfg) + if err != nil { + slog.Error("failed to generate jwt token", "error", err) + return Admin{}, apperrors.ErrInternalServer + } + + admin := Admin{ + User: User(adminInfo), + JwtToken: jwtToken, + } + + return admin, nil +} diff --git a/backend/internal/app/contribution/domain.go b/backend/internal/app/contribution/domain.go index daaea56b..0fc11c51 100644 --- a/backend/internal/app/contribution/domain.go +++ b/backend/internal/app/contribution/domain.go @@ -73,3 +73,8 @@ type FetchUserContributionsResponse struct { Contribution Repository } + +type ConfigureContributionTypeScore struct { + ContributionType string `json:"contributionType"` + Score int `json:"score"` +} \ No newline at end of file diff --git a/backend/internal/app/contribution/handler.go b/backend/internal/app/contribution/handler.go index 49ea7bf3..dfac84c2 100644 --- a/backend/internal/app/contribution/handler.go +++ b/backend/internal/app/contribution/handler.go @@ -1,6 +1,7 @@ package contribution import ( + "encoding/json" "log/slog" "net/http" @@ -17,6 +18,8 @@ type handler struct { type Handler interface { FetchUserContributions(w http.ResponseWriter, r *http.Request) ListMonthlyContributionSummary(w http.ResponseWriter, r *http.Request) + ListAllContributionTypes(w http.ResponseWriter, r *http.Request) + ConfigureContributionTypeScore(w http.ResponseWriter, r *http.Request) } func NewHandler(contributionService Service) Handler { @@ -88,3 +91,48 @@ func (h *handler) ListMonthlyContributionSummary(w http.ResponseWriter, r *http. response.WriteJson(w, http.StatusOK, "contribution type overview for month fetched successfully", monthlyContributionSummary) } + +func (h *handler) ListAllContributionTypes(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + contributionTypes, err := h.contributionService.ListAllContributionTypes(ctx) + if err != nil { + slog.Error("error fetching all contribution types", "error", err) + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) + return + } + + response.WriteJson(w, http.StatusOK, "all contribution types fetched successfully", contributionTypes) +} + +func (h *handler) ConfigureContributionTypeScore(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + // isAdminValue := ctx.Value(middleware.IsAdminKey) + // isAdmin, ok := isAdminValue.(bool) + // if !ok { + // slog.Error("error verifying admin from context") + // status, errorMessage := apperrors.MapError(apperrors.ErrContextValue) + // response.WriteJson(w, status, errorMessage, nil) + // return + // } + + var configureContributionTypeScores []ConfigureContributionTypeScore + err := json.NewDecoder(r.Body).Decode(&configureContributionTypeScores) + if err != nil { + slog.Error(apperrors.ErrFailedMarshal.Error(), "error", err) + response.WriteJson(w, http.StatusBadRequest, apperrors.ErrInvalidRequestBody.Error(), nil) + return + } + + contributionTypeScores, err := h.contributionService.ConfigureContributionTypeScore(ctx, configureContributionTypeScores) + if err != nil { + slog.Error("error configuring contribution type scores", "error", err) + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) + return + } + + response.WriteJson(w, http.StatusOK, "contribution types fscores configured successfully", contributionTypeScores) +} diff --git a/backend/internal/app/contribution/service.go b/backend/internal/app/contribution/service.go index 9a7e85c0..900d871f 100644 --- a/backend/internal/app/contribution/service.go +++ b/backend/internal/app/contribution/service.go @@ -67,6 +67,8 @@ type Service interface { FetchUserContributions(ctx context.Context, userId int) ([]FetchUserContributionsResponse, error) GetContributionByGithubEventId(ctx context.Context, githubEventId string) (Contribution, error) ListMonthlyContributionSummary(ctx context.Context, year int, monthParam int, userId int) ([]MonthlyContributionSummary, error) + ListAllContributionTypes(ctx context.Context) ([]ContributionScore, error) + ConfigureContributionTypeScore(ctx context.Context, configureContributionTypeScore []ConfigureContributionTypeScore) ([]ContributionScore, error) } func NewService(bigqueryService bigquery.Service, contributionRepository repository.ContributionRepository, repositoryService repoService.Service, userService user.Service, transactionService transaction.Service, httpClient *http.Client) Service { @@ -310,3 +312,38 @@ func (s *service) ListMonthlyContributionSummary(ctx context.Context, year int, return serviceMonthlyContributionSummaries, nil } + +func (s *service) ListAllContributionTypes(ctx context.Context) ([]ContributionScore, error) { + contributionTypes, err := s.contributionRepository.GetAllContributionTypes(ctx, nil) + if err != nil { + slog.Error("error fetching all contribution types", "error", err) + return nil, err + } + + serviceContributionTypes := make([]ContributionScore, len(contributionTypes)) + for i, c := range contributionTypes { + serviceContributionTypes[i] = ContributionScore(c) + } + + return serviceContributionTypes, nil +} + +func (s *service) ConfigureContributionTypeScore(ctx context.Context, configureContributionTypeScore []ConfigureContributionTypeScore) ([]ContributionScore, error) { + repoConfigureContributionScore := make([]repository.ConfigureContributionTypeScore, len(configureContributionTypeScore)) + for i, c := range configureContributionTypeScore { + repoConfigureContributionScore[i] = repository.ConfigureContributionTypeScore(c) + } + + contributionTypeScores, err := s.contributionRepository.UpdateContributionTypeScore(ctx, nil, repoConfigureContributionScore) + if err != nil { + slog.Error("error updating contritbution types score", "error", err) + return nil, err + } + + serviceContributionTypeScores := make([]ContributionScore, len(contributionTypeScores)) + for i, c := range contributionTypeScores { + serviceContributionTypeScores[i] = ContributionScore(c) + } + + return serviceContributionTypeScores, nil +} diff --git a/backend/internal/app/goal/domain.go b/backend/internal/app/goal/domain.go index e822e84f..9d3e482d 100644 --- a/backend/internal/app/goal/domain.go +++ b/backend/internal/app/goal/domain.go @@ -24,3 +24,9 @@ type CustomGoalLevelTarget struct { ContributionType string `json:"contributionType"` Target int `json:"target"` } + +type UserGoalLevelProgress struct { + ContributionType string `json:"contributionType"` + TargetCount int `json:"targetCount"` + AchievedCount int `json:"achievedCount"` +} diff --git a/backend/internal/app/goal/handler.go b/backend/internal/app/goal/handler.go index 4fb12277..d1637404 100644 --- a/backend/internal/app/goal/handler.go +++ b/backend/internal/app/goal/handler.go @@ -16,9 +16,9 @@ type handler struct { type Handler interface { ListGoalLevels(w http.ResponseWriter, r *http.Request) - ListGoalLevelTargets(w http.ResponseWriter, r *http.Request) + GetUserActiveGoalLevel(w http.ResponseWriter, r *http.Request) CreateCustomGoalLevelTarget(w http.ResponseWriter, r *http.Request) - ListGoalLevelAchievedTarget(w http.ResponseWriter, r *http.Request) + ListUserGoalLevelProgress(w http.ResponseWriter, r *http.Request) } func NewHandler(goalService Service) Handler { @@ -41,7 +41,7 @@ func (h *handler) ListGoalLevels(w http.ResponseWriter, r *http.Request) { response.WriteJson(w, http.StatusOK, "goal levels fetched successfully", gaols) } -func (h *handler) ListGoalLevelTargets(w http.ResponseWriter, r *http.Request) { +func (h *handler) GetUserActiveGoalLevel(w http.ResponseWriter, r *http.Request) { ctx := r.Context() userIdCtxVal := ctx.Value(middleware.UserIdKey) @@ -53,15 +53,15 @@ func (h *handler) ListGoalLevelTargets(w http.ResponseWriter, r *http.Request) { return } - goalLevelTargets, err := h.goalService.ListGoalLevelTargetDetail(ctx, userId) + userGoalLevel, err := h.goalService.GetUserActiveGoalLevel(ctx, userId) if err != nil { - slog.Error("error fetching goal level targets", "error", err) + slog.Error("error fetching users active goal level", "error", err) status, errorMessage := apperrors.MapError(err) response.WriteJson(w, status, errorMessage, nil) return } - response.WriteJson(w, http.StatusOK, "goal level targets fetched successfully", goalLevelTargets) + response.WriteJson(w, http.StatusOK, "user active goal level fetched successfully", userGoalLevel) } func (h *handler) CreateCustomGoalLevelTarget(w http.ResponseWriter, r *http.Request) { @@ -94,7 +94,7 @@ func (h *handler) CreateCustomGoalLevelTarget(w http.ResponseWriter, r *http.Req response.WriteJson(w, http.StatusOK, "custom goal level targets created successfully", createdCustomGoalLevelTargets) } -func (h *handler) ListGoalLevelAchievedTarget(w http.ResponseWriter, r *http.Request) { +func (h *handler) ListUserGoalLevelProgress(w http.ResponseWriter, r *http.Request) { ctx := r.Context() userIdCtxVal := ctx.Value(middleware.UserIdKey) @@ -106,12 +106,13 @@ func (h *handler) ListGoalLevelAchievedTarget(w http.ResponseWriter, r *http.Req return } - goalLevelAchievedTarget, err := h.goalService.ListGoalLevelAchievedTarget(ctx, userId) + userGoalLevelProgress, err := h.goalService.ListUserGoalLevelProgress(ctx, userId) if err != nil { - slog.Error("error failed to list goal level achieved targets", "error", err) + slog.Error("error failed to fetch user goal level progress", "error", err) response.WriteJson(w, http.StatusBadRequest, err.Error(), nil) return } - response.WriteJson(w, http.StatusOK, "goal level achieved targets fetched successfully", goalLevelAchievedTarget) + response.WriteJson(w, http.StatusOK, "user goal level progress fetched successfully", userGoalLevelProgress) + } diff --git a/backend/internal/app/goal/service.go b/backend/internal/app/goal/service.go index 33a71f39..1824c912 100644 --- a/backend/internal/app/goal/service.go +++ b/backend/internal/app/goal/service.go @@ -18,9 +18,9 @@ type service struct { type Service interface { ListGoalLevels(ctx context.Context) ([]Goal, error) GetGoalIdByGoalLevel(ctx context.Context, level string) (int, error) - ListGoalLevelTargetDetail(ctx context.Context, userId int) ([]GoalContribution, error) + GetUserActiveGoalLevel(ctx context.Context, userId int) (string, error) CreateCustomGoalLevelTarget(ctx context.Context, userId int, customGoalLevelTarget []CustomGoalLevelTarget) ([]GoalContribution, error) - ListGoalLevelAchievedTarget(ctx context.Context, userId int) (map[string]int, error) + ListUserGoalLevelProgress(ctx context.Context, userId int) ([]UserGoalLevelProgress, error) } func NewService(goalRepository repository.GoalRepository, contributionRepository repository.ContributionRepository, badgeService badge.Service) Service { @@ -58,19 +58,14 @@ func (s *service) GetGoalIdByGoalLevel(ctx context.Context, level string) (int, return goalId, err } -func (s *service) ListGoalLevelTargetDetail(ctx context.Context, userId int) ([]GoalContribution, error) { - goalLevelTargets, err := s.goalRepository.ListUserGoalLevelTargets(ctx, nil, userId) +func (s *service) GetUserActiveGoalLevel(ctx context.Context, userId int) (string, error) { + userGoalLevel, err := s.goalRepository.GetUserActiveGoalLevel(ctx, nil, userId) if err != nil { - slog.Error("error fetching goal level targets", "error", err) - return nil, err - } - - serviceGoalLevelTargets := make([]GoalContribution, len(goalLevelTargets)) - for i, g := range goalLevelTargets { - serviceGoalLevelTargets[i] = GoalContribution(g) + slog.Error("error fetching user active gaol level", "error", err) + return "", err } - return serviceGoalLevelTargets, nil + return userGoalLevel, nil } func (s *service) CreateCustomGoalLevelTarget(ctx context.Context, userId int, customGoalLevelTarget []CustomGoalLevelTarget) ([]GoalContribution, error) { @@ -107,24 +102,14 @@ func (s *service) CreateCustomGoalLevelTarget(ctx context.Context, userId int, c return goalContributions, nil } -func (s *service) ListGoalLevelAchievedTarget(ctx context.Context, userId int) (map[string]int, error) { + +func (s *service) ListUserGoalLevelProgress(ctx context.Context, userId int) ([]UserGoalLevelProgress, error) { goalLevelSetTargets, err := s.goalRepository.ListUserGoalLevelTargets(ctx, nil, userId) if err != nil { slog.Error("error fetching goal level targets", "error", err) return nil, err } - contributionTypes := make([]CustomGoalLevelTarget, len(goalLevelSetTargets)) - for i, g := range goalLevelSetTargets { - contributionTypes[i].ContributionType, err = s.contributionRepository.GetContributionTypeByContributionScoreId(ctx, nil, g.ContributionScoreId) - if err != nil { - slog.Error("error fetching contribution type by contribution score id", "error", err) - return nil, err - } - - contributionTypes[i].Target = g.TargetCount - } - year := int(time.Now().Year()) month := int(time.Now().Month()) monthlyContributionCount, err := s.contributionRepository.ListMonthlyContributionSummary(ctx, nil, year, month, userId) @@ -133,32 +118,43 @@ func (s *service) ListGoalLevelAchievedTarget(ctx context.Context, userId int) ( return nil, err } - contributionsAchievedTarget := make(map[string]int, len(monthlyContributionCount)) - + contributionCountMap := make(map[string]int) for _, m := range monthlyContributionCount { - contributionsAchievedTarget[m.Type] = m.Count + contributionCountMap[m.Type] = m.Count } - var completedTarget int - for _, c := range contributionTypes { - if c.Target == contributionsAchievedTarget[c.ContributionType] { - completedTarget += 1 - } - } + userGoalLevelProgress := make([]UserGoalLevelProgress, len(goalLevelSetTargets)) + var contributionsCompleted int - if completedTarget == len(goalLevelSetTargets) { - userGoalLevel, err := s.goalRepository.GetUserActiveGoalLevel(ctx, nil, userId) + for i, g := range goalLevelSetTargets { + contributionType, err := s.contributionRepository.GetContributionTypeByContributionScoreId(ctx, nil, g.ContributionScoreId) if err != nil { - slog.Error("error fetching user active gaol level", "error", err) + slog.Error("error") return nil, err } - _, err = s.badgeService.HandleBadgeCreation(ctx, userId, userGoalLevel) - if err != nil { - slog.Error("error handling user badge creation", "error", err) - return nil, err + userGoalLevelProgress[i].ContributionType = contributionType + userGoalLevelProgress[i].TargetCount = g.TargetCount + userGoalLevelProgress[i].AchievedCount = contributionCountMap[contributionType] + + if userGoalLevelProgress[i].AchievedCount == g.TargetCount { + contributionsCompleted++ + } + + if contributionsCompleted == len(goalLevelSetTargets) { + userGoalLevel, err := s.goalRepository.GetUserActiveGoalLevel(ctx, nil, userId) + if err != nil { + slog.Error("error fetching user active gaol level", "error", err) + return nil, err + } + + _, err = s.badgeService.HandleBadgeCreation(ctx, userId, userGoalLevel) + if err != nil { + slog.Error("error handling user badge creation", "error", err) + return nil, err + } } } - return contributionsAchievedTarget, nil + return userGoalLevelProgress, nil } diff --git a/backend/internal/app/router.go b/backend/internal/app/router.go index 13ce566a..4679bdbb 100644 --- a/backend/internal/app/router.go +++ b/backend/internal/app/router.go @@ -17,12 +17,15 @@ func NewRouter(deps Dependencies) http.Handler { router.HandleFunc("GET /api/v1/auth/github", deps.AuthHandler.GithubOAuthLoginUrl) router.HandleFunc("GET /api/v1/auth/github/callback", deps.AuthHandler.GithubOAuthLoginCallback) router.HandleFunc("GET /api/v1/auth/user", middleware.Authentication(deps.AuthHandler.GetLoggedInUser, deps.AppCfg)) + router.HandleFunc("GET /api/v1/auth/admin", deps.AuthHandler.LoginAdmin) router.HandleFunc("PATCH /api/v1/user/email", middleware.Authentication(deps.UserHandler.UpdateUserEmail, deps.AppCfg)) router.HandleFunc("DELETE /api/v1/user/delete/{user_id}", middleware.Authentication(deps.UserHandler.SoftDeleteUser, deps.AppCfg)) router.HandleFunc("GET /api/v1/user/contributions/all", middleware.Authentication(deps.ContributionHandler.FetchUserContributions, deps.AppCfg)) router.HandleFunc("GET /api/v1/user/overview", middleware.Authentication(deps.ContributionHandler.ListMonthlyContributionSummary, deps.AppCfg)) + router.HandleFunc("GET /api/v1/contributions/types", middleware.Authentication(deps.ContributionHandler.ListAllContributionTypes, deps.AppCfg)) + router.HandleFunc("PATCH /api/v1/contributions/scores/configure", middleware.Authentication(middleware.AuthorizeAdmin(deps.ContributionHandler.ConfigureContributionTypeScore), deps.AppCfg)) router.HandleFunc("GET /api/v1/user/repositories", middleware.Authentication(deps.RepositoryHandler.FetchUsersContributedRepos, deps.AppCfg)) router.HandleFunc("GET /api/v1/user/repositories/{repo_id}", middleware.Authentication(deps.RepositoryHandler.FetchParticularRepoDetails, deps.AppCfg)) @@ -33,11 +36,11 @@ func NewRouter(deps Dependencies) http.Handler { router.HandleFunc("GET /api/v1/leaderboard", middleware.Authentication(deps.UserHandler.ListUserRanks, deps.AppCfg)) router.HandleFunc("GET /api/v1/user/leaderboard", middleware.Authentication(deps.UserHandler.GetCurrentUserRank, deps.AppCfg)) - router.HandleFunc("GET /api/v1/user/goal/level", middleware.Authentication(deps.GoalHandler.ListGoalLevels, deps.AppCfg)) + router.HandleFunc("GET /api/v1/goal/level", middleware.Authentication(deps.GoalHandler.ListGoalLevels, deps.AppCfg)) router.HandleFunc("PATCH /api/v1/user/goal/level", middleware.Authentication(deps.UserHandler.UpdateCurrentActiveGoalId, deps.AppCfg)) - router.HandleFunc("GET /api/v1/user/goal/level/targets", middleware.Authentication(deps.GoalHandler.ListGoalLevelTargets, deps.AppCfg)) + router.HandleFunc("GET /api/v1/user/goal/level", middleware.Authentication(deps.GoalHandler.GetUserActiveGoalLevel, deps.AppCfg)) router.HandleFunc("POST /api/v1/user/goal/level/custom/targets", middleware.Authentication(deps.GoalHandler.CreateCustomGoalLevelTarget, deps.AppCfg)) - router.HandleFunc("GET /api/v1/user/goal/level/targets/achieved", middleware.Authentication(deps.GoalHandler.ListGoalLevelAchievedTarget, deps.AppCfg)) + router.HandleFunc("GET /api/v1/user/goal/level/progress", middleware.Authentication(deps.GoalHandler.ListUserGoalLevelProgress, deps.AppCfg)) router.HandleFunc("GET /api/v1/user/badges", middleware.Authentication(deps.BadgeHandler.GetBadgeDetailsOfUser, deps.AppCfg)) diff --git a/backend/internal/app/user/domain.go b/backend/internal/app/user/domain.go index 4599fe80..3b9d91cb 100644 --- a/backend/internal/app/user/domain.go +++ b/backend/internal/app/user/domain.go @@ -58,3 +58,8 @@ type LeaderboardUser struct { type GoalLevel struct { Level string `json:"level"` } + +type AdminLoginRequest struct { + Email string + Password string +} diff --git a/backend/internal/app/user/service.go b/backend/internal/app/user/service.go index 2e58ba79..a41f97a5 100644 --- a/backend/internal/app/user/service.go +++ b/backend/internal/app/user/service.go @@ -30,6 +30,7 @@ type Service interface { GetAllUsersRank(ctx context.Context) ([]LeaderboardUser, error) GetCurrentUserRank(ctx context.Context, userId int) (LeaderboardUser, error) UpdateCurrentActiveGoalId(ctx context.Context, userId int, level string) (int, error) + GetLoggedInAdmin(ctx context.Context, adminInfo AdminLoginRequest) (User, error) } func NewService(userRepository repository.UserRepository, goalService goal.Service, repositoryService repoService.Service) Service { @@ -200,3 +201,13 @@ func (s *service) UpdateCurrentActiveGoalId(ctx context.Context, userId int, lev return goalId, err } + +func (s *service) GetLoggedInAdmin(ctx context.Context, adminInfo AdminLoginRequest) (User, error) { + admin, err := s.userRepository.GetAdminByCredentials(ctx, nil, repository.AdminLoginRequest(adminInfo)) + if err != nil { + slog.Error("failed to verify admin credentials", "error", err) + return User{}, err + } + + return User(admin), nil +} diff --git a/backend/internal/pkg/apperrors/errors.go b/backend/internal/pkg/apperrors/errors.go index c1e60415..ff32c722 100644 --- a/backend/internal/pkg/apperrors/errors.go +++ b/backend/internal/pkg/apperrors/errors.go @@ -58,6 +58,8 @@ var ( ErrCustomGoalTargetCreationFailed = errors.New("failed to create targets for custom goal level") ErrBadgeCreationFailed = errors.New("failed to create badge for user") + + ErrInvalidCredentials = errors.New("error invalid credentials") ) func MapError(err error) (statusCode int, errMessage string) { diff --git a/backend/internal/pkg/middleware/middleware.go b/backend/internal/pkg/middleware/middleware.go index 4c407894..3f0a9f3d 100644 --- a/backend/internal/pkg/middleware/middleware.go +++ b/backend/internal/pkg/middleware/middleware.go @@ -72,3 +72,21 @@ func Authentication(next http.HandlerFunc, appCfg config.AppConfig) http.Handler next.ServeHTTP(w, r) }) } + +func AuthorizeAdmin(next http.HandlerFunc) http.HandlerFunc { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + isAdmin, ok := ctx.Value(IsAdminKey).(bool) + if !ok { + response.WriteJson(w, http.StatusInternalServerError, apperrors.ErrContextValue.Error(), nil) + return + } + + if !isAdmin { + response.WriteJson(w, http.StatusUnauthorized, apperrors.ErrUnauthorizedAccess.Error(), nil) + return + } + + next.ServeHTTP(w, r) + }) +} diff --git a/backend/internal/repository/contribution.go b/backend/internal/repository/contribution.go index d3618464..2d4e17a0 100644 --- a/backend/internal/repository/contribution.go +++ b/backend/internal/repository/contribution.go @@ -23,6 +23,7 @@ type ContributionRepository interface { GetAllContributionTypes(ctx context.Context, tx *sqlx.Tx) ([]ContributionScore, error) ListMonthlyContributionSummary(ctx context.Context, tx *sqlx.Tx, year int, month int, userId int) ([]MonthlyContributionSummary, error) GetContributionTypeByContributionScoreId(ctx context.Context, tx *sqlx.Tx, contributionScoreId int) (string, error) + UpdateContributionTypeScore(ctx context.Context, tx *sqlx.Tx, configureContributionTypeScore []ConfigureContributionTypeScore) ([]ContributionScore, error) } func NewContributionRepository(db *sqlx.DB) ContributionRepository { @@ -66,6 +67,8 @@ const ( month, contribution_type;` getContributionTypeByContributionScoreIdQuery = `SELECT contribution_type from contribution_score where id=$1` + + updateContributionTypeScoreQuery = "UPDATE contribution_score SET score = $1 where contribution_type = $2" ) func (cr *contributionRepository) CreateContribution(ctx context.Context, tx *sqlx.Tx, contributionInfo Contribution) (Contribution, error) { @@ -176,3 +179,24 @@ func (cr *contributionRepository) GetContributionTypeByContributionScoreId(ctx c return contributionType, nil } + +func (cr *contributionRepository) UpdateContributionTypeScore(ctx context.Context, tx *sqlx.Tx, configureContributionTypeScore []ConfigureContributionTypeScore) ([]ContributionScore, error) { + executer := cr.BaseRepository.initiateQueryExecuter(tx) + + for _, c := range configureContributionTypeScore { + _, err := executer.ExecContext(ctx, updateContributionTypeScoreQuery, c.Score, c.ContributionType) + if err != nil { + slog.Error("failed to update score for contribution type", "error", err) + return nil, apperrors.ErrInternalServer + } + } + + var contributionTypeScores []ContributionScore + err := executer.SelectContext(ctx, &contributionTypeScores, getAllContributionTypesQuery) + if err != nil { + slog.Error("error fetching all contribution type scores", "error", err) + return nil, apperrors.ErrFetchingContributionTypes + } + + return contributionTypeScores, nil +} diff --git a/backend/internal/repository/domain.go b/backend/internal/repository/domain.go index c3faf145..b31e667d 100644 --- a/backend/internal/repository/domain.go +++ b/backend/internal/repository/domain.go @@ -119,3 +119,13 @@ type Badge struct { CreatedAt time.Time `db:"created_at"` UpdatedAt time.Time `db:"updated_at"` } + +type AdminLoginRequest struct { + Email string `db:"email"` + Password string `db:"password"` +} + +type ConfigureContributionTypeScore struct { + ContributionType string `db:"contribution_type"` + Score int `db:"score"` +} diff --git a/backend/internal/repository/goal.go b/backend/internal/repository/goal.go index f56bc5e2..ef5a59d0 100644 --- a/backend/internal/repository/goal.go +++ b/backend/internal/repository/goal.go @@ -131,6 +131,11 @@ func (gr *goalRepository) GetUserActiveGoalLevel(ctx context.Context, tx *sqlx.T var userActiveGoalLevel string err := executer.GetContext(ctx, &userActiveGoalLevel, getUserActiveGoalLevelQuery, userId) if err != nil { + if errors.Is(err, sql.ErrNoRows) { + slog.Info("user does not have any active goal level") + return "", nil + } + slog.Error("error getting users current active goal level name", "error", err) return userActiveGoalLevel, apperrors.ErrInternalServer } diff --git a/backend/internal/repository/user.go b/backend/internal/repository/user.go index e75db674..758fe167 100644 --- a/backend/internal/repository/user.go +++ b/backend/internal/repository/user.go @@ -29,6 +29,7 @@ type UserRepository interface { GetAllUsersRank(ctx context.Context, tx *sqlx.Tx) ([]LeaderboardUser, error) GetCurrentUserRank(ctx context.Context, tx *sqlx.Tx, userId int) (LeaderboardUser, error) UpdateCurrentActiveGoalId(ctx context.Context, tx *sqlx.Tx, userId int, goalId int) (int, error) + GetAdminByCredentials(ctx context.Context, tx *sqlx.Tx, adminInfo AdminLoginRequest) (User, error) } func NewUserRepository(db *sqlx.DB) UserRepository { @@ -92,6 +93,8 @@ const ( WHERE id = $1;` updateCurrentActiveGoalIdQuery = "UPDATE users SET current_active_goal_id=$1 where id=$2" + + verifyAdminCredentialsQuery = "SELECT * FROM users where email = $1 and is_admin=true" ) func (ur *userRepository) GetUserById(ctx context.Context, tx *sqlx.Tx, userId int) (User, error) { @@ -260,3 +263,20 @@ func (ur *userRepository) UpdateCurrentActiveGoalId(ctx context.Context, tx *sql return goalId, nil } + +func (ur *userRepository) GetAdminByCredentials(ctx context.Context, tx *sqlx.Tx, adminInfo AdminLoginRequest) (User, error) { + executer := ur.BaseRepository.initiateQueryExecuter(tx) + + var admin User + err := executer.GetContext(ctx, &admin, verifyAdminCredentialsQuery, adminInfo.Email) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + slog.Error("invalid admin credentials", "error", err) + return User{}, apperrors.ErrInvalidCredentials + } + slog.Error("failed to verify admin credentials", "error", err) + return User{}, apperrors.ErrInternalServer + } + + return admin, nil +} From c9663b281f912ffc54566e0c02bec453c7210171 Mon Sep 17 00:00:00 2001 From: VishakhaSainani-Josh Date: Mon, 4 Aug 2025 23:04:44 +0530 Subject: [PATCH 15/36] implement block or unblock user feature --- backend/internal/app/auth/handler.go | 24 ++++++++++++++ backend/internal/app/router.go | 5 ++- backend/internal/app/user/domain.go | 8 +++-- backend/internal/app/user/handler.go | 48 ++++++++++++++++++++++++++++ backend/internal/app/user/service.go | 28 ++++++++++++++++ backend/internal/repository/user.go | 33 ++++++++++++++++++- 6 files changed, 142 insertions(+), 4 deletions(-) diff --git a/backend/internal/app/auth/handler.go b/backend/internal/app/auth/handler.go index 3d40c859..e9091616 100644 --- a/backend/internal/app/auth/handler.go +++ b/backend/internal/app/auth/handler.go @@ -1,6 +1,7 @@ package auth import ( + "encoding/json" "fmt" "log/slog" "net/http" @@ -20,6 +21,7 @@ type Handler interface { GithubOAuthLoginUrl(w http.ResponseWriter, r *http.Request) GithubOAuthLoginCallback(w http.ResponseWriter, r *http.Request) GetLoggedInUser(w http.ResponseWriter, r *http.Request) + LoginAdmin(w http.ResponseWriter, r *http.Request) } func NewHandler(authService Service, appConfig config.AppConfig) Handler { @@ -82,3 +84,25 @@ func (h *handler) GetLoggedInUser(w http.ResponseWriter, r *http.Request) { response.WriteJson(w, http.StatusOK, "logged in user fetched successfully", userInfo) } + +func (h *handler) LoginAdmin(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + var requestBody AdminLoginRequest + err := json.NewDecoder(r.Body).Decode(&requestBody) + if err != nil { + slog.Error(apperrors.ErrFailedMarshal.Error(), "error", err) + response.WriteJson(w, http.StatusBadRequest, apperrors.ErrInvalidRequestBody.Error(), nil) + return + } + + adminInfo, err := h.authService.VerifyAdminCredentials(ctx, requestBody) + if err != nil { + slog.Error("failed to verify admin credentials", "error", err) + status, errorMessage := apperrors.MapError(apperrors.ErrContextValue) + response.WriteJson(w, status, errorMessage, nil) + return + } + + response.WriteJson(w, http.StatusOK, "admin logged in successfully", adminInfo) +} diff --git a/backend/internal/app/router.go b/backend/internal/app/router.go index 4679bdbb..2fc197b7 100644 --- a/backend/internal/app/router.go +++ b/backend/internal/app/router.go @@ -25,7 +25,6 @@ func NewRouter(deps Dependencies) http.Handler { router.HandleFunc("GET /api/v1/user/contributions/all", middleware.Authentication(deps.ContributionHandler.FetchUserContributions, deps.AppCfg)) router.HandleFunc("GET /api/v1/user/overview", middleware.Authentication(deps.ContributionHandler.ListMonthlyContributionSummary, deps.AppCfg)) router.HandleFunc("GET /api/v1/contributions/types", middleware.Authentication(deps.ContributionHandler.ListAllContributionTypes, deps.AppCfg)) - router.HandleFunc("PATCH /api/v1/contributions/scores/configure", middleware.Authentication(middleware.AuthorizeAdmin(deps.ContributionHandler.ConfigureContributionTypeScore), deps.AppCfg)) router.HandleFunc("GET /api/v1/user/repositories", middleware.Authentication(deps.RepositoryHandler.FetchUsersContributedRepos, deps.AppCfg)) router.HandleFunc("GET /api/v1/user/repositories/{repo_id}", middleware.Authentication(deps.RepositoryHandler.FetchParticularRepoDetails, deps.AppCfg)) @@ -44,5 +43,9 @@ func NewRouter(deps Dependencies) http.Handler { router.HandleFunc("GET /api/v1/user/badges", middleware.Authentication(deps.BadgeHandler.GetBadgeDetailsOfUser, deps.AppCfg)) + router.HandleFunc("PATCH /api/v1/contributions/scores/configure", middleware.Authentication(middleware.AuthorizeAdmin(deps.ContributionHandler.ConfigureContributionTypeScore), deps.AppCfg)) + router.HandleFunc("GET /api/v1/users", middleware.Authentication(middleware.AuthorizeAdmin(deps.UserHandler.ListAllUsers), deps.AppCfg)) + router.HandleFunc("PATCH /api/v1/users/{user_id}", middleware.Authentication(middleware.AuthorizeAdmin(deps.UserHandler.BlockOrUnblockUser), deps.AppCfg)) + return middleware.CorsMiddleware(router, deps.AppCfg) } diff --git a/backend/internal/app/user/domain.go b/backend/internal/app/user/domain.go index 3b9d91cb..8c9a9cc0 100644 --- a/backend/internal/app/user/domain.go +++ b/backend/internal/app/user/domain.go @@ -60,6 +60,10 @@ type GoalLevel struct { } type AdminLoginRequest struct { - Email string - Password string + Email string `json:"email"` + Password string `json:"password"` +} + +type BlockOrUnblockUserRequest struct { + Block bool `json:"block"` } diff --git a/backend/internal/app/user/handler.go b/backend/internal/app/user/handler.go index 8133c9f9..28f01a44 100644 --- a/backend/internal/app/user/handler.go +++ b/backend/internal/app/user/handler.go @@ -4,6 +4,7 @@ import ( "encoding/json" "log/slog" "net/http" + "strconv" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/middleware" @@ -20,6 +21,8 @@ type Handler interface { ListUserRanks(w http.ResponseWriter, r *http.Request) GetCurrentUserRank(w http.ResponseWriter, r *http.Request) UpdateCurrentActiveGoalId(w http.ResponseWriter, r *http.Request) + ListAllUsers(w http.ResponseWriter, r *http.Request) + BlockOrUnblockUser(w http.ResponseWriter, r *http.Request) } func NewHandler(userService Service) Handler { @@ -149,3 +152,48 @@ func (h *handler) UpdateCurrentActiveGoalId(w http.ResponseWriter, r *http.Reque response.WriteJson(w, http.StatusOK, "Goal updated successfully", goalId) } + +func (h *handler) ListAllUsers(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + users, err := h.userService.ListAllUsers(ctx) + if err != nil { + slog.Error("failed to fetch all users", "error", err) + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) + return + } + + response.WriteJson(w, http.StatusOK, "users fetched successfully", users) +} + +func (h *handler) BlockOrUnblockUser(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + userIdPath := r.PathValue("user_id") + userId, err := strconv.Atoi(userIdPath) + if err != nil { + slog.Error("error getting user id from request url", "error", err) + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) + return + } + + var status BlockOrUnblockUserRequest + err = json.NewDecoder(r.Body).Decode(&status) + if err != nil { + slog.Error(apperrors.ErrFailedMarshal.Error(), "error", err) + response.WriteJson(w, http.StatusBadRequest, apperrors.ErrInvalidRequestBody.Error(), nil) + return + } + + err = h.userService.BlockOrUnblockUser(ctx, userId, status.Block) + if err != nil { + slog.Error("failed to block/unblock user", "error", err) + status, message := apperrors.MapError(err) + response.WriteJson(w, status, message, nil) + return + } + + response.WriteJson(w, http.StatusOK, "user status updated successfully", nil) +} diff --git a/backend/internal/app/user/service.go b/backend/internal/app/user/service.go index a41f97a5..160777eb 100644 --- a/backend/internal/app/user/service.go +++ b/backend/internal/app/user/service.go @@ -31,6 +31,8 @@ type Service interface { GetCurrentUserRank(ctx context.Context, userId int) (LeaderboardUser, error) UpdateCurrentActiveGoalId(ctx context.Context, userId int, level string) (int, error) GetLoggedInAdmin(ctx context.Context, adminInfo AdminLoginRequest) (User, error) + ListAllUsers(ctx context.Context) ([]User, error) + BlockOrUnblockUser(ctx context.Context, userID int, block bool) error } func NewService(userRepository repository.UserRepository, goalService goal.Service, repositoryService repoService.Service) Service { @@ -211,3 +213,29 @@ func (s *service) GetLoggedInAdmin(ctx context.Context, adminInfo AdminLoginRequ return User(admin), nil } + +func (s *service) ListAllUsers(ctx context.Context) ([]User, error) { + users, err := s.userRepository.GetAllUsers(ctx, nil) + if err != nil { + slog.Error("failed to fetch all users", "error", err) + return nil, apperrors.ErrInternalServer + } + + serviceUsers := make([]User, len(users)) + + for i, u := range users { + serviceUsers[i] = User(u) + } + + return serviceUsers, nil +} + +func (s *service) BlockOrUnblockUser(ctx context.Context, userID int, block bool) error { + err := s.userRepository.UpdateUserBlockStatus(ctx, nil, userID, block) + if err != nil { + slog.Error("failed to block/unblock user", "error", err) + return err + } + + return nil +} diff --git a/backend/internal/repository/user.go b/backend/internal/repository/user.go index 758fe167..7333694e 100644 --- a/backend/internal/repository/user.go +++ b/backend/internal/repository/user.go @@ -30,6 +30,8 @@ type UserRepository interface { GetCurrentUserRank(ctx context.Context, tx *sqlx.Tx, userId int) (LeaderboardUser, error) UpdateCurrentActiveGoalId(ctx context.Context, tx *sqlx.Tx, userId int, goalId int) (int, error) GetAdminByCredentials(ctx context.Context, tx *sqlx.Tx, adminInfo AdminLoginRequest) (User, error) + GetAllUsers(ctx context.Context, tx *sqlx.Tx) ([]User, error) + UpdateUserBlockStatus(ctx context.Context, tx *sqlx.Tx, userID int, block bool) error } func NewUserRepository(db *sqlx.DB) UserRepository { @@ -61,7 +63,7 @@ const ( hardDeleteUsersQuery = "DELETE FROM users WHERE is_deleted = TRUE AND deleted_at <= $1" - getAllUsersGithubIdQuery = "SELECT github_id from users" + getAllUsersGithubIdQuery = "SELECT github_id from users where is_admin=false" updateUserCurrentBalanceQuery = "UPDATE users SET current_balance=$1, updated_at=$2 where id=$3" @@ -95,6 +97,10 @@ const ( updateCurrentActiveGoalIdQuery = "UPDATE users SET current_active_goal_id=$1 where id=$2" verifyAdminCredentialsQuery = "SELECT * FROM users where email = $1 and is_admin=true" + + getAllUsersQuery = "SELECT * FROM users where is_admin=false" + + updateUserBlockStatusQuery = "UPDATE users SET is_blocked=$1 where id=$2" ) func (ur *userRepository) GetUserById(ctx context.Context, tx *sqlx.Tx, userId int) (User, error) { @@ -280,3 +286,28 @@ func (ur *userRepository) GetAdminByCredentials(ctx context.Context, tx *sqlx.Tx return admin, nil } + +func (ur *userRepository) GetAllUsers(ctx context.Context, tx *sqlx.Tx) ([]User, error) { + executer := ur.BaseRepository.initiateQueryExecuter(tx) + + var users []User + err := executer.SelectContext(ctx, &users, getAllUsersQuery) + if err != nil { + slog.Error("error occurred while getting all users", "error", err) + return nil, apperrors.ErrInternalServer + } + + return users, nil +} + +func (ur *userRepository) UpdateUserBlockStatus(ctx context.Context, tx *sqlx.Tx, userID int, block bool) error { + executer := ur.BaseRepository.initiateQueryExecuter(tx) + + _, err := executer.ExecContext(ctx, updateUserBlockStatusQuery, block, userID) + if err != nil { + slog.Error("failed to update user block status", "error", err) + return apperrors.ErrInternalServer + } + + return nil +} From c8e31bb56f70b7ebd96451e39ca3d8aeb45d3fc7 Mon Sep 17 00:00:00 2001 From: VishakhaSainani-Josh Date: Tue, 5 Aug 2025 00:06:41 +0530 Subject: [PATCH 16/36] latest code --- backend/internal/app/contribution/service.go | 55 ++-- frontend/package.json | 2 + frontend/src/api/queries/Admin.ts | 26 ++ frontend/src/api/queries/UserGoals.ts | 125 +++++++ .../src/api/queries/UserProfileDetails.ts | 51 ++- frontend/src/assets/Coin.svg | 9 + frontend/src/assets/bronzeBadge.svg | 9 + frontend/src/assets/goldBadge.svg | 9 + frontend/src/assets/silverBadge.svg | 9 + .../Login/components/AdminLoginComponent.tsx | 70 ++++ .../Login/components/LoginComponent.tsx | 2 +- .../components/Repositories.tsx | 9 +- .../components/Contributors.tsx | 5 +- .../components/Repository.tsx | 2 +- .../components/RepositoryActivities.tsx | 86 ++--- .../components/RepositoryCard.tsx | 6 +- .../UserDashboard/components/Overview.tsx | 7 +- .../components/RecentActivities.tsx | 11 +- frontend/src/index.css | 2 + .../UserDashboard/DeleteAccount.tsx | 64 ++++ .../components/UserDashboard/Navbar.tsx | 2 +- .../UserDashboard/SettingsDialog.tsx | 65 ++++ .../components/UserDashboard/UserBadges.tsx | 26 +- .../components/UserDashboard/UserEmail.tsx | 57 ++++ .../components/UserDashboard/UserGoals.tsx | 306 ++++++++++++++++-- .../UserDashboard/UserProfileCard.tsx | 2 +- .../UserDashboard/UserProfileDetails.tsx | 87 +++-- .../UserDashboard/UserProfileMenu.tsx | 37 +++ .../shared/components/common/ActivityCard.tsx | 6 +- .../src/shared/components/common/Coin.tsx | 6 +- .../shared/components/common/CoinsInfo.tsx | 72 +++++ frontend/src/shared/components/ui/button.tsx | 8 +- frontend/src/shared/components/ui/input.tsx | 21 ++ .../src/shared/components/ui/scroll-area.tsx | 56 ++++ frontend/src/shared/components/ui/select.tsx | 183 +++++++++++ frontend/src/shared/constants/query-keys.ts | 4 + frontend/src/shared/layout/AdminLayout.tsx | 66 ++++ frontend/src/shared/layout/AuthLayout.tsx | 9 +- frontend/src/shared/types/types.ts | 67 ++++ frontend/tailwind.config.js | 1 + 40 files changed, 1468 insertions(+), 172 deletions(-) create mode 100644 frontend/src/api/queries/Admin.ts create mode 100644 frontend/src/api/queries/UserGoals.ts create mode 100644 frontend/src/assets/Coin.svg create mode 100644 frontend/src/assets/bronzeBadge.svg create mode 100644 frontend/src/assets/goldBadge.svg create mode 100644 frontend/src/assets/silverBadge.svg create mode 100644 frontend/src/features/Login/components/AdminLoginComponent.tsx create mode 100644 frontend/src/shared/components/UserDashboard/DeleteAccount.tsx create mode 100644 frontend/src/shared/components/UserDashboard/SettingsDialog.tsx create mode 100644 frontend/src/shared/components/UserDashboard/UserEmail.tsx create mode 100644 frontend/src/shared/components/UserDashboard/UserProfileMenu.tsx create mode 100644 frontend/src/shared/components/common/CoinsInfo.tsx create mode 100644 frontend/src/shared/components/ui/input.tsx create mode 100644 frontend/src/shared/components/ui/scroll-area.tsx create mode 100644 frontend/src/shared/components/ui/select.tsx create mode 100644 frontend/src/shared/layout/AdminLayout.tsx diff --git a/backend/internal/app/contribution/service.go b/backend/internal/app/contribution/service.go index 900d871f..2a658ed7 100644 --- a/backend/internal/app/contribution/service.go +++ b/backend/internal/app/contribution/service.go @@ -17,22 +17,23 @@ import ( // github event names const ( - pullRequestEvent = "PullRequestEvent" - issuesEvent = "IssuesEvent" - pushEvent = "PushEvent" - issueCommentEvent = "IssueCommentEvent" + pullRequestEvent = "PullRequestEvent" + issuesEvent = "IssuesEvent" + issueCommentEvent = "IssueCommentEvent" + pullRequestCommentEvent = "PullRequestReviewCommentEvent" + pullRequestReviewEvent = "PullRequestReviewEvent" ) // app contribution types const ( - pullRequestMerged = "PullRequestMerged" - pullRequestOpened = "PullRequestOpened" - issueOpened = "IssueOpened" - issueClosed = "IssueClosed" - issueResolved = "IssueResolved" - pullRequestUpdated = "PullRequestUpdated" - issueComment = "IssueComment" - pullRequestComment = "PullRequestComment" + pullRequestMerged = "PullRequestMerged" + pullRequestOpened = "PullRequestOpened" + issueOpened = "IssueOpened" + issueClosed = "IssueClosed" + issueResolved = "IssueResolved" + issueComment = "IssueComment" + pullRequestComment = "PullRequestComment" + pullRequestReviewed = "PullRequestReviewed" ) // payload @@ -46,6 +47,8 @@ const ( PayloadOpenedKey = "opened" PayloadNotPlannedKey = "not_planned" PayloadCompletedKey = "completed" + PayloadCreatedKey = "created" + PayloadApprovedKey = "approved" ) type service struct { @@ -112,7 +115,7 @@ func (s *service) ProcessFetchedContributions(ctx context.Context) error { err := s.ProcessEachContribution(ctx, contribution) if err != nil { slog.Error("error processing contribution with github event id", "github event id", "error", contribution.ID, err) - return err + continue } } @@ -165,14 +168,18 @@ func (s *service) GetContributionType(ctx context.Context, contribution Contribu var isMerged bool if pullRequestPayload, ok := contributionPayload[payloadPullRequestKey]; ok { pullRequest = pullRequestPayload.(map[string]interface{}) - isMerged = pullRequest[PayloadMergedKey].(bool) + if isMergedVal, ok := pullRequest[PayloadMergedKey]; ok { + isMerged = isMergedVal.(bool) + } } var issue map[string]interface{} var stateReason string if issuePayload, ok := contributionPayload[PayloadIssueKey]; ok { issue = issuePayload.(map[string]interface{}) - stateReason = issue[PayloadStateReasonKey].(string) + if stateReasonVal, ok := issue[PayloadStateReasonKey]; ok { + stateReason = stateReasonVal.(string) + } } var contributionType string @@ -193,14 +200,22 @@ func (s *service) GetContributionType(ctx context.Context, contribution Contribu contributionType = issueResolved } - case pushEvent: - contributionType = pullRequestUpdated + // case pushEvent: + // contributionType = pullRequestUpdated + + case pullRequestReviewEvent: + if action == PayloadCreatedKey || action == PayloadApprovedKey { + contributionType = pullRequestReviewed + } case issueCommentEvent: contributionType = issueComment - case pullRequestComment: - contributionType = pullRequestComment + //if user.login not equal to contribution login + case pullRequestCommentEvent: + if action == PayloadCreatedKey { + contributionType = pullRequestComment + } } return contributionType, nil @@ -241,7 +256,7 @@ func (s *service) HandleContributionCreation(ctx context.Context, repositoryID i } contributionType, err := s.GetContributionType(ctx, contribution) - if err != nil { + if err != nil || contributionType == "" { slog.Error("error getting contribution type", "error", err) return Contribution{}, err } diff --git a/frontend/package.json b/frontend/package.json index 465c6238..1bc19d05 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -18,6 +18,8 @@ "@radix-ui/react-dialog": "^1.1.14", "@radix-ui/react-dropdown-menu": "^2.1.15", "@radix-ui/react-progress": "^1.1.7", + "@radix-ui/react-scroll-area": "^1.2.9", + "@radix-ui/react-select": "^2.2.5", "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slot": "^1.2.3", "@tailwindcss/vite": "^4.1.11", diff --git a/frontend/src/api/queries/Admin.ts b/frontend/src/api/queries/Admin.ts new file mode 100644 index 00000000..91441a3d --- /dev/null +++ b/frontend/src/api/queries/Admin.ts @@ -0,0 +1,26 @@ +import { BACKEND_URL } from "@/shared/constants/endpoints"; +import type { ApiResponse } from "@/shared/types/api"; +import { api } from "../axios"; +import { useMutation } from "@tanstack/react-query"; +import type { AdminCredentials } from "@/shared/types/types"; + +const LogInAdmin = async ( + adminCredentials: AdminCredentials +): Promise> => { + const response = await api.patch<{ + message: string; + data: null; + }>(`${BACKEND_URL}/api/v1/auth/admin`, { + email: adminCredentials.email, + password: adminCredentials.password + }); + + return response.data; +}; + +export const useLogInAdmin = () => { + return useMutation({ + mutationFn: (adminCredentials: AdminCredentials) => + LogInAdmin(adminCredentials) + }); +}; diff --git a/frontend/src/api/queries/UserGoals.ts b/frontend/src/api/queries/UserGoals.ts new file mode 100644 index 00000000..24b400d4 --- /dev/null +++ b/frontend/src/api/queries/UserGoals.ts @@ -0,0 +1,125 @@ +import type { ApiResponse } from "@/shared/types/api"; +import { api } from "../axios"; +import { BACKEND_URL } from "@/shared/constants/endpoints"; +import { useMutation, useQuery } from "@tanstack/react-query"; +import { + CONTRIBUTION_TYPES_QUERY_KEY, + GOAL_LEVELS_QUERY_KEY, + USER_ACTIVE_GOAL_LEVEL_QUERY_KEY, + USER_GOAL_LEVEL_PROGRESS_QUERY_KEY +} from "@/shared/constants/query-keys"; +import type { + ContributionTypeDetail, + CustomGoalLevelTarget, + CustomGoalLevelTargetResponse, + GoalLevel, + GoalLevelProgress +} from "@/shared/types/types"; + +const fetchUserActiveGoalLevel = async (): Promise> => { + const response = await api.get<{ + message: string; + data: string; + }>(`${BACKEND_URL}/api/v1/user/goal/level`); + + return response.data; +}; + +export const useUserActiveGoalLevel = () => { + return useQuery({ + queryKey: [USER_ACTIVE_GOAL_LEVEL_QUERY_KEY], + queryFn: fetchUserActiveGoalLevel + }); +}; + +const setUserGoalLevel = async ( + selectedLevel: string +): Promise> => { + const response = await api.patch<{ + message: string; + data: number; + }>(`${BACKEND_URL}/api/v1/user/goal/level`, { + level: selectedLevel + }); + + return response.data; +}; + +export const useSetUserGoalLevel = () => { + return useMutation({ + mutationFn: (selectedLevel: string) => setUserGoalLevel(selectedLevel) + }); +}; + +const fetchGoalLevels = async (): Promise> => { + const response = await api.get<{ + message: string; + data: GoalLevel[]; + }>(`${BACKEND_URL}/api/v1/goal/level`); + + return response.data; +}; + +export const useGoalLevels = () => { + return useQuery({ + queryKey: [GOAL_LEVELS_QUERY_KEY], + queryFn: fetchGoalLevels + }); +}; + +const fetchUserGoalLevelProgress = async (): Promise< + ApiResponse +> => { + const response = await api.get<{ + message: string; + data: GoalLevelProgress[]; + }>(`${BACKEND_URL}/api/v1/user/goal/level/progress`); + + return response.data; +}; + +export const useUserGoalLevelProgress = () => { + return useQuery({ + queryKey: [USER_GOAL_LEVEL_PROGRESS_QUERY_KEY], + queryFn: fetchUserGoalLevelProgress + }); +}; + +const createCustomGoalLevelTarget = async ( + customGoalLevelTarget: CustomGoalLevelTarget[] +): Promise> => { + const response = await api.post<{ + message: string; + data: CustomGoalLevelTargetResponse[]; + }>( + `${BACKEND_URL}/api/v1/user/goal/level/custom/targets`, + customGoalLevelTarget + ); + + return response.data; +}; + +export const useCustomGoalLevelTarget = () => { + return useMutation({ + mutationFn: (customGoalLevelTarget: CustomGoalLevelTarget[]) => + createCustomGoalLevelTarget(customGoalLevelTarget) + }); +}; + +const fetchAllContributionTypes = async (): Promise< + ApiResponse +> => { + const response = await api.get<{ + message: string; + data: ContributionTypeDetail[]; + }>(`${BACKEND_URL}/api/v1/contributions/types`); + + return response.data; +}; + +export const useAllContributionTypes = () => { + return useQuery({ + queryKey: [CONTRIBUTION_TYPES_QUERY_KEY], + queryFn: fetchAllContributionTypes + }); +}; diff --git a/frontend/src/api/queries/UserProfileDetails.ts b/frontend/src/api/queries/UserProfileDetails.ts index 3d4639ed..9811fa4c 100644 --- a/frontend/src/api/queries/UserProfileDetails.ts +++ b/frontend/src/api/queries/UserProfileDetails.ts @@ -1,22 +1,53 @@ import type { User } from "@/shared/types/types"; import { api } from "../axios"; -import { useQuery } from "@tanstack/react-query"; +import { useMutation, useQuery } from "@tanstack/react-query"; import type { ApiResponse } from "@/shared/types/api"; import { LOGGED_IN_USER_QUERY_KEY } from "@/shared/constants/query-keys"; import { BACKEND_URL } from "@/shared/constants/endpoints"; const fetchLoggedInUser = async (): Promise> => { - const response = await api.get<{ - message: string; - data: User; - }>(`${BACKEND_URL}/api/v1/auth/user`); + const response = await api.get<{ + message: string; + data: User; + }>(`${BACKEND_URL}/api/v1/auth/user`); - return response.data; + return response.data; }; export const useLoggedInUser = () => { - return useQuery({ - queryKey: [LOGGED_IN_USER_QUERY_KEY], - queryFn: fetchLoggedInUser, - }); + return useQuery({ + queryKey: [LOGGED_IN_USER_QUERY_KEY], + queryFn: fetchLoggedInUser + }); +}; + +const updateUserEmail = async (email: string): Promise> => { + const response = await api.patch<{ + message: string; + data: null; + }>(`${BACKEND_URL}/api/v1/user/email`, { + email: email + }); + + return response.data; +}; + +export const useUpdateUserEmail = () => { + return useMutation({ + mutationFn: (email: string) => updateUserEmail(email) + }); +}; + +const softDeleteUser = async (userId: number): Promise> => { + const response = await api.delete<{ message: string; data: null }>( + `${BACKEND_URL}/api/v1/user/delete/${userId}` + ); + + return response.data; +}; + +export const useSoftDeleteUser = () => { + return useMutation({ + mutationFn: (userId: number) => softDeleteUser(userId) + }); }; diff --git a/frontend/src/assets/Coin.svg b/frontend/src/assets/Coin.svg new file mode 100644 index 00000000..476c3625 --- /dev/null +++ b/frontend/src/assets/Coin.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/frontend/src/assets/bronzeBadge.svg b/frontend/src/assets/bronzeBadge.svg new file mode 100644 index 00000000..019636b3 --- /dev/null +++ b/frontend/src/assets/bronzeBadge.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/frontend/src/assets/goldBadge.svg b/frontend/src/assets/goldBadge.svg new file mode 100644 index 00000000..04073e45 --- /dev/null +++ b/frontend/src/assets/goldBadge.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/frontend/src/assets/silverBadge.svg b/frontend/src/assets/silverBadge.svg new file mode 100644 index 00000000..50a59821 --- /dev/null +++ b/frontend/src/assets/silverBadge.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/frontend/src/features/Login/components/AdminLoginComponent.tsx b/frontend/src/features/Login/components/AdminLoginComponent.tsx new file mode 100644 index 00000000..cbc09b39 --- /dev/null +++ b/frontend/src/features/Login/components/AdminLoginComponent.tsx @@ -0,0 +1,70 @@ +// import { type FC } from "react"; +// import type { AdminCredentials } from "@/shared/types/types"; +// import { useLogInAdmin } from "@/api/queries/Admin"; + +// const AdminLogin: FC = () => { +// const { +// register, +// handleSubmit, +// formState: { errors }, +// } = useForm(); + +// const { mutate, isPending, isError, error } = useLogInAdmin(); + +// const onSubmit = (data: AdminCredentials) => { +// mutate(data); +// }; + +// return ( +//
+//
+// +// +// {errors.email && ( +//

{errors.email.message}

+// )} +//
+ +//
+// +// +// {errors.password && ( +//

{errors.password.message}

+// )} +//
+ +// {isError && ( +//

+// {(error as Error)?.message || "Failed to log in"} +//

+// )} + +// +//
+// ); +// }; + +// export default AdminLogin; diff --git a/frontend/src/features/Login/components/LoginComponent.tsx b/frontend/src/features/Login/components/LoginComponent.tsx index a76bf347..d5fd489f 100644 --- a/frontend/src/features/Login/components/LoginComponent.tsx +++ b/frontend/src/features/Login/components/LoginComponent.tsx @@ -14,7 +14,7 @@ const LoginComponent = () => { window.location.href = GITHUB_AUTH_URL || ""; localStorage.setItem( ACCESS_TOKEN_KEY, - "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjIsIklzQWRtaW4iOmZhbHNlLCJleHAiOjE3NTQxMzI3NTh9.VKEboNEvSeVKYnqLuBrvTyvx9IglhYzEyeE57x7Qzto" + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjIsIklzQWRtaW4iOmZhbHNlLCJleHAiOjE3NTQzNzUwMDF9.FL-pl22Idc5Ge2iEiXTxYx5c1WBH06GZg5AYoonHiuI" ); }; diff --git a/frontend/src/features/MyContributions/components/Repositories.tsx b/frontend/src/features/MyContributions/components/Repositories.tsx index b7fb3461..ccc575b9 100644 --- a/frontend/src/features/MyContributions/components/Repositories.tsx +++ b/frontend/src/features/MyContributions/components/Repositories.tsx @@ -3,9 +3,14 @@ import { Separator } from "@/shared/components/ui/separator"; import RepositoriesCard from "./RepositoriesCard"; const Repositories = () => { - const { data } = useRepositories(); + const { data, isLoading } = useRepositories(); const repositoriesData = data?.data; - + if (isLoading) + return ( +
+
+
+ ); return (
{repositoriesData?.map(repo => ( diff --git a/frontend/src/features/RepositoryDetails.tsx/components/Contributors.tsx b/frontend/src/features/RepositoryDetails.tsx/components/Contributors.tsx index 75824937..70c01cc4 100644 --- a/frontend/src/features/RepositoryDetails.tsx/components/Contributors.tsx +++ b/frontend/src/features/RepositoryDetails.tsx/components/Contributors.tsx @@ -20,9 +20,9 @@ const ContributorsList = () => { const contributorsData = viewAll ? contributors : contributors?.slice(0, 20); return ( -
+
-

+

Contributors {contributors.length}

- - {isLoading ? ( -
-
-
- ) : repositoryActivitiesData?.length === 0 ? ( -
- -

- No recent activities found -

-
- ) : ( -
- {repositoryActivitiesData?.map((activity, index) => ( - - ))} - {!viewAll && ( -
- - How does points work? - -
- )} -
- )} + {content} ); }; diff --git a/frontend/src/features/RepositoryDetails.tsx/components/RepositoryCard.tsx b/frontend/src/features/RepositoryDetails.tsx/components/RepositoryCard.tsx index ec53fac5..a0d4365e 100644 --- a/frontend/src/features/RepositoryDetails.tsx/components/RepositoryCard.tsx +++ b/frontend/src/features/RepositoryDetails.tsx/components/RepositoryCard.tsx @@ -21,15 +21,15 @@ const RepositoryCard: FC = ({ return (
-
+
{name} - +

diff --git a/frontend/src/features/UserDashboard/components/Overview.tsx b/frontend/src/features/UserDashboard/components/Overview.tsx index daeb90b1..e5fb648f 100644 --- a/frontend/src/features/UserDashboard/components/Overview.tsx +++ b/frontend/src/features/UserDashboard/components/Overview.tsx @@ -91,12 +91,7 @@ const Overview: FC = ({ className }) => {

- No overview data -

-

- No activity found for the selected period. -
- Try selecting a different month or start contributing! + No overview data for the selected period.

) : ( diff --git a/frontend/src/features/UserDashboard/components/RecentActivities.tsx b/frontend/src/features/UserDashboard/components/RecentActivities.tsx index 5b016b4c..ab0366e7 100644 --- a/frontend/src/features/UserDashboard/components/RecentActivities.tsx +++ b/frontend/src/features/UserDashboard/components/RecentActivities.tsx @@ -4,8 +4,8 @@ import { Button } from "@/shared/components/ui/button"; import { Card } from "@/shared/components/ui/card"; import ActivityCard from "@/shared/components/common/ActivityCard"; import { useRecentActivities } from "@/api/queries/RecentActivities"; -import { Link } from "react-router-dom"; import { TrendingUp } from "lucide-react"; +import CoinsInfo from "@/shared/components/common/CoinsInfo"; interface RecentActivitiesProps { className?: string; @@ -72,13 +72,8 @@ const RecentActivities: FC = ({ className }) => { /> ))} {!viewAll && ( -
- - How does points work? - +
+
)}
diff --git a/frontend/src/index.css b/frontend/src/index.css index d277bc17..91621762 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -62,6 +62,7 @@ --cc-app-blue: oklch(0.3876 0.1761 261.76); --cc-app-gray-background: oklch(0.9585 0.0195 270.21); --cc-app-orange: oklch(0.7362 0.1641 62.07); + --cc-app-yellow: oklch(0.9136 0.174 99.92); } @media (prefers-color-scheme: light) { @@ -116,6 +117,7 @@ --color-cc-app-blue: var(--cc-app-blue); --color-cc-app-gray-background: var(--cc-app-gray-background); --color-cc-app-orange: var(--cc-app-orange); + --color-cc-app-yellow: var(--cc-app-yellow); } .dark { diff --git a/frontend/src/shared/components/UserDashboard/DeleteAccount.tsx b/frontend/src/shared/components/UserDashboard/DeleteAccount.tsx new file mode 100644 index 00000000..c3d114cc --- /dev/null +++ b/frontend/src/shared/components/UserDashboard/DeleteAccount.tsx @@ -0,0 +1,64 @@ +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, + DialogDescription +} from "@/shared/components/ui/dialog"; +import { Button } from "@/shared/components/ui/button"; +import { + useLoggedInUser, + useSoftDeleteUser +} from "@/api/queries/UserProfileDetails"; + +interface Props { + open: boolean; + onClose: () => void; +} + +const DeleteAccount = ({ open, onClose }: Props) => { + const { data } = useLoggedInUser(); + const user = data?.data; + const { mutate: softDeleteUser, isPending } = useSoftDeleteUser(); + + const handleDelete = () => { + if (!user?.userId) return; + softDeleteUser(user.userId, { + onSuccess: () => { + onClose(); + } + }); + }; + + return ( + + + + Delete Account + + Deleting your account will not immediately erase your data. We will + retain your information for 3 months in case you choose to return. + After that period, your data will be permanently removed from our + platform. + + + + + + + + + + ); +}; + +export default DeleteAccount; diff --git a/frontend/src/shared/components/UserDashboard/Navbar.tsx b/frontend/src/shared/components/UserDashboard/Navbar.tsx index b509d225..b660ce34 100644 --- a/frontend/src/shared/components/UserDashboard/Navbar.tsx +++ b/frontend/src/shared/components/UserDashboard/Navbar.tsx @@ -17,7 +17,7 @@ const Navbar = () => { className={`rounded-full px-6 py-3 font-medium text-white transition-colors duration-200 ${ isActive(option.path) ? "bg-cc-app-mid-blue" - : "hover:bg-cc-app-mid-blue" + : "hover:bg-[#003aa5]" } `} > {option.name} diff --git a/frontend/src/shared/components/UserDashboard/SettingsDialog.tsx b/frontend/src/shared/components/UserDashboard/SettingsDialog.tsx new file mode 100644 index 00000000..e71011de --- /dev/null +++ b/frontend/src/shared/components/UserDashboard/SettingsDialog.tsx @@ -0,0 +1,65 @@ +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter +} from "@/shared/components/ui/dialog"; +import { Button } from "@/shared/components/ui/button"; +import { useState } from "react"; +import DeleteAccount from "./DeleteAccount"; + +interface Props { + open: boolean; + onClose: () => void; + onUpdateEmail: () => void; + +} + +const SettingsDialog = ({ + open, + onClose, + onUpdateEmail, +}: Props) => { + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); + + return ( + + + + Account Settings + + +
+ + + + setShowDeleteConfirm(false)} + /> +
+ + + + +
+
+ ); +}; + +export default SettingsDialog; diff --git a/frontend/src/shared/components/UserDashboard/UserBadges.tsx b/frontend/src/shared/components/UserDashboard/UserBadges.tsx index 0cc35a75..e42547fe 100644 --- a/frontend/src/shared/components/UserDashboard/UserBadges.tsx +++ b/frontend/src/shared/components/UserDashboard/UserBadges.tsx @@ -1,12 +1,13 @@ import { useUserBadges } from "@/api/queries/UserBadges"; -import { Star } from "lucide-react"; import type { Badge } from "@/shared/types/types"; +import bronzeBadge from "@/assets/bronzeBadge.svg"; +import silverBadge from "@/assets/silverBadge.svg"; +import goldBadge from "@/assets/goldBadge.svg"; const badgeColorMap: Record = { - BEGINNER: "text-[#cd7f32]", - INTERMEDIATE: "text-[#c0c0c0]", - ADVANCED: "text-[#ffd700]", - CUSTOM: "text-orange-400", + BEGINNER: bronzeBadge, + INTERMEDIATE: silverBadge, + ADVANCED: goldBadge }; const UserBadges = () => { @@ -27,16 +28,17 @@ const UserBadges = () => {

{Object.entries(grouped).map(([type, badgeList]) => { - const color = badgeColorMap[type] ?? "text-gray-400"; + const badge = badgeColorMap[type] ?? ""; return ( -
- +
+ Badge {badgeList.length > 1 && ( - Ă—{badgeList.length} + Ă—{badgeList.length} )} -
+
{type}
diff --git a/frontend/src/shared/components/UserDashboard/UserEmail.tsx b/frontend/src/shared/components/UserDashboard/UserEmail.tsx new file mode 100644 index 00000000..e876d0a3 --- /dev/null +++ b/frontend/src/shared/components/UserDashboard/UserEmail.tsx @@ -0,0 +1,57 @@ +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter +} from "@/shared/components/ui/dialog"; +import { Input } from "@/shared/components/ui/input"; +import { Button } from "@/shared/components/ui/button"; +import { useState } from "react"; +import { useUpdateUserEmail } from "@/api/queries/UserProfileDetails"; + +interface Props { + defaultEmail: string; + onClose: () => void; +} + +const UserEmail = ({ defaultEmail, onClose }: Props) => { + const [email, setEmail] = useState(defaultEmail); + const { mutate: updateEmail, isPending } = useUpdateUserEmail(); + + const isValidEmail = (email: string) => + /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); + + const handleUpdate = () => { + if (!isValidEmail(email)) return; + updateEmail(email, { + onSuccess: () => onClose() + }); + }; + + return ( + + + + Update Email + + + setEmail(e.target.value)} + placeholder="Enter new email" + className="selection:bg-cc-app-blue" + /> + + + + + + + ); +}; + +export default UserEmail; diff --git a/frontend/src/shared/components/UserDashboard/UserGoals.tsx b/frontend/src/shared/components/UserDashboard/UserGoals.tsx index f6cdd95c..b6713868 100644 --- a/frontend/src/shared/components/UserDashboard/UserGoals.tsx +++ b/frontend/src/shared/components/UserDashboard/UserGoals.tsx @@ -1,40 +1,290 @@ +import { useState } from "react"; import { Progress } from "@/shared/components/ui/progress"; -const goals = [ - { name: "Issue Resolve", current: 2, total: 5, progress: 40 }, - { name: "PR Review", current: 6, total: 8, progress: 75 }, - { name: "PR Merge", current: 2, total: 2, progress: 100 }, - { name: "PR Close", current: 1, total: 5, progress: 20 }, - { name: "PR Close", current: 1, total: 5, progress: 20 }, - { name: "PR Close", current: 1, total: 5, progress: 20 }, - { name: "PR Close", current: 1, total: 5, progress: 20 }, - { name: "PR Close", current: 1, total: 5, progress: 20 }, - { name: "PR Close", current: 1, total: 5, progress: 20 }, - { name: "PR Close", current: 1, total: 5, progress: 20 } -]; +import { Button } from "@/shared/components/ui/button"; +import { + Dialog, + DialogTrigger, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter +} from "@/shared/components/ui/dialog"; +import { Loader2 } from "lucide-react"; +import { + useAllContributionTypes, + useCustomGoalLevelTarget, + useGoalLevels, + useSetUserGoalLevel, + useUserActiveGoalLevel, + useUserGoalLevelProgress +} from "@/api/queries/UserGoals"; +import { useQueryClient } from "@tanstack/react-query"; +import { + USER_ACTIVE_GOAL_LEVEL_QUERY_KEY, + USER_GOAL_LEVEL_PROGRESS_QUERY_KEY +} from "@/shared/constants/query-keys"; +import type { + ContributionTypeDetail, + CustomGoalLevelTarget +} from "@/shared/types/types"; const UserGoals = () => { + const [dialogOpen, setDialogOpen] = useState(false); + const [isSettingLevel, setIsSettingLevel] = useState(false); + const [isCustomDialogOpen, setIsCustomDialogOpen] = useState(false); + + const [customGoals, setCustomGoals] = useState([]); + const [selectedType, setSelectedType] = useState(""); + const [target, setTarget] = useState(""); + + const { data: userGoalLevelRes, isLoading: isGoalLevelLoading } = + useUserActiveGoalLevel(); + const { data: goalLevelsRes, isLoading: isGoalLevelsLoading } = + useGoalLevels(); + const { data: userProgressRes, isLoading: isProgressLoading } = + useUserGoalLevelProgress(); + const { mutate: setGoalLevel } = useSetUserGoalLevel(); + const { data: contributionTypesRes } = useAllContributionTypes(); + const { mutate: setCustomTarget, isPending: isSettingCustom } = + useCustomGoalLevelTarget(); + + const queryClient = useQueryClient(); + + const userLevel = userGoalLevelRes?.data ?? ""; + const goalLevels = goalLevelsRes?.data ?? []; + const userProgress = userProgressRes?.data ?? []; + const allTypes: ContributionTypeDetail[] = contributionTypesRes?.data ?? []; + + const handleLevelSelect = (level: string) => { + setIsSettingLevel(true); + setGoalLevel(level, { + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: [USER_ACTIVE_GOAL_LEVEL_QUERY_KEY] + }); + queryClient.invalidateQueries({ + queryKey: [USER_GOAL_LEVEL_PROGRESS_QUERY_KEY] + }); + + setIsSettingLevel(false); + + if (level.toLowerCase() === "custom") { + setDialogOpen(false); // close default dialog + setIsCustomDialogOpen(true); // open custom dialog + } else { + setDialogOpen(false); + } + }, + onError: () => { + console.error("Failed to set user goal level"); + setIsSettingLevel(false); + } + }); + }; + + const handleAddCustomGoal = () => { + if (!selectedType || !target) return; + if (customGoals.some(g => g.contributionType === selectedType)) return; + + setCustomGoals(prev => [ + ...prev, + { + contributionType: selectedType, + target: Number(target) + } + ]); + setSelectedType(""); + setTarget(""); + }; + + const handleSubmitCustomGoals = () => { + setCustomTarget(customGoals, { + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: [USER_ACTIVE_GOAL_LEVEL_QUERY_KEY] + }); + queryClient.invalidateQueries({ + queryKey: [USER_GOAL_LEVEL_PROGRESS_QUERY_KEY] + }); + setIsCustomDialogOpen(false); + setCustomGoals([]); + }, + onError: () => { + console.error("Failed to set custom goal targets"); + } + }); + }; + + if (isGoalLevelLoading || isGoalLevelsLoading || isProgressLoading) { + return ( +
+ + Loading goals... +
+ ); + } + return (

- MY GOALS (BEGINNER) + MY GOALS {userLevel && `(${userLevel.toUpperCase()})`}

-
- {goals.map((goal, index) => ( -
-
- {goal.name} - - {goal.current}/{goal.total} - + + {userLevel ? ( +
+ {userProgress.map((goal, index) => { + const percent = goal.targetCount + ? Math.min((goal.achievedCount / goal.targetCount) * 100, 100) + : 0; + + return ( +
+
+ + {goal.contributionType.replace(/([A-Z])/g, " $1")} + + + {goal.achievedCount}/{goal.targetCount} + +
+ +
+ ); + })} +
+ ) : ( +
+

No Active Goal Set

+

+ You haven't selected a goal level for this month yet. Choose a level + to start tracking your contributions. +

+ + + + + + + + Select Goal Level for the month + + + {!isSettingLevel ? ( +
+ {goalLevels.map(level => ( + + ))} +
+ ) : ( +
+ + Setting your goal... +
+ )} + + + + +
+
+
+ )} + + + + + Set Custom Contribution Goals + + +
+
+ + setTarget(e.target.value)} + /> +
- + + {customGoals.length > 0 && ( +
+ {customGoals.map((goal, idx) => ( +
+ {goal.contributionType} + {goal.target} + +
+ ))} +
+ )}
- ))} -
+ + + + + + +
); }; diff --git a/frontend/src/shared/components/UserDashboard/UserProfileCard.tsx b/frontend/src/shared/components/UserDashboard/UserProfileCard.tsx index 36918e54..42bed017 100644 --- a/frontend/src/shared/components/UserDashboard/UserProfileCard.tsx +++ b/frontend/src/shared/components/UserDashboard/UserProfileCard.tsx @@ -8,7 +8,7 @@ const UserProfileCard = () => { return (
-
+
{Array.from({ length: 30 }).map((_, i) => (
diff --git a/frontend/src/shared/components/UserDashboard/UserProfileDetails.tsx b/frontend/src/shared/components/UserDashboard/UserProfileDetails.tsx index 111faf49..c105babd 100644 --- a/frontend/src/shared/components/UserDashboard/UserProfileDetails.tsx +++ b/frontend/src/shared/components/UserDashboard/UserProfileDetails.tsx @@ -1,60 +1,99 @@ -import { ExternalLink, MoreHorizontal } from "lucide-react"; +import { useState } from "react"; +import { ExternalLink, MoreVertical } from "lucide-react"; import Coin from "@/shared/components/common/Coin"; import { Button } from "@/shared/components/ui/button"; -import DefaultProfilePic from "@/assets/default-profile-pic.svg" +import DefaultProfilePic from "@/assets/default-profile-pic.svg"; import { Separator } from "@/shared/components/ui/separator"; import { useLoggedInUser } from "@/api/queries/UserProfileDetails"; import { Link } from "react-router-dom"; +import UserProfileMenu from "./UserProfileMenu"; +import UserEmail from "./UserEmail"; +import SettingsDialog from "./SettingsDialog"; + const UserProfileDetails = () => { const { data } = useLoggedInUser(); - const user = data?.data + const user = data?.data; + + const [showSettingsDialog, setShowSettingsDialog] = useState(false); + const [showEmailDialog, setShowEmailDialog] = useState(false); + + const handleSettingsClick = () => setShowSettingsDialog(true); + const handleLogoutClick = () => console.log("Logout clicked"); + + const handleDeleteAccount = () => { + console.log("Delete account clicked"); + setShowSettingsDialog(false); + }; + + const handleUpdateEmail = () => { + setShowSettingsDialog(false); + setShowEmailDialog(true); + }; return (
-
-
- Profile +
+
+ Profile +
+
+ +
+
-

{user?.githubUsername || "username"}

- - +

+ {user?.githubUsername || "username"} +

+
+ +
- {user?.currentBalance || "0"} + + {user?.currentBalance || "0"} +
- -
+ + setShowSettingsDialog(false)} + onUpdateEmail={handleUpdateEmail} + /> + {showEmailDialog && ( + setShowEmailDialog(false)} + /> + )}
); }; diff --git a/frontend/src/shared/components/UserDashboard/UserProfileMenu.tsx b/frontend/src/shared/components/UserDashboard/UserProfileMenu.tsx new file mode 100644 index 00000000..4da83bf9 --- /dev/null +++ b/frontend/src/shared/components/UserDashboard/UserProfileMenu.tsx @@ -0,0 +1,37 @@ +import { MoreVertical } from "lucide-react"; +import { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, +} from "@/shared/components/ui/dropdown-menu"; +import { Button } from "@/shared/components/ui/button"; + +interface Props { + onSettingsClick: () => void; + onLogoutClick: () => void; +} + +const UserProfileMenu = ({ onSettingsClick, onLogoutClick }: Props) => { + return ( + + + + + + Settings + + Logout + + + ); +}; + +export default UserProfileMenu; diff --git a/frontend/src/shared/components/common/ActivityCard.tsx b/frontend/src/shared/components/common/ActivityCard.tsx index fb473e9e..e3c87ff1 100644 --- a/frontend/src/shared/components/common/ActivityCard.tsx +++ b/frontend/src/shared/components/common/ActivityCard.tsx @@ -30,14 +30,12 @@ const ActivityCard: FC = ({
- {contributionType} + {contributionType.replace(/([A-Z])/g, " $1")}
{isRepositoryActivity ? null : (
- Contributed to - + Contributed to <{repositoryName}> -
)} diff --git a/frontend/src/shared/components/common/Coin.tsx b/frontend/src/shared/components/common/Coin.tsx index e85fdbdf..0ba372f8 100644 --- a/frontend/src/shared/components/common/Coin.tsx +++ b/frontend/src/shared/components/common/Coin.tsx @@ -1,8 +1,8 @@ +import coinSvg from "@/assets/Coin.svg"; + const Coin = () => { return ( -
-
-
+ Coin ); }; diff --git a/frontend/src/shared/components/common/CoinsInfo.tsx b/frontend/src/shared/components/common/CoinsInfo.tsx new file mode 100644 index 00000000..2a621904 --- /dev/null +++ b/frontend/src/shared/components/common/CoinsInfo.tsx @@ -0,0 +1,72 @@ +import { + Dialog, + DialogTrigger, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/shared/components/ui/dialog"; +import { Button } from "@/shared/components/ui/button"; +import { useAllContributionTypes } from "@/api/queries/UserGoals"; + +const CoinsInfo = () => { + const { data, isLoading } = useAllContributionTypes(); + const contributions = data?.data ?? []; + + return ( + + + + + + + + How does the point system work? + + + +
+ We assign coins for every open-source contribution you make on GitHub. + Contributions are updated everyday at midnight, so you can see + contributions you made before 12 am yesterday +
+ +
+ Current Point Structure: +
+ +
+ {isLoading ? ( +
Loading...
+ ) : contributions.length === 0 ? ( +
+ No data available. +
+ ) : ( +
    + {contributions.map(c => ( +
  • + {c.contributionType} + + {c.score} pts + +
  • + ))} +
+ )} +
+ + +
+
+ ); +}; + +export default CoinsInfo; diff --git a/frontend/src/shared/components/ui/button.tsx b/frontend/src/shared/components/ui/button.tsx index 61ab42c9..8a384cab 100644 --- a/frontend/src/shared/components/ui/button.tsx +++ b/frontend/src/shared/components/ui/button.tsx @@ -14,16 +14,18 @@ const buttonVariants = cva( destructive: "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", outline: - "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", + "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 hover:cursor-pointer", secondary: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", link: "text-primary underline-offset-4 hover:underline", ccAppOutlineMidBlue: - "bg-cc-app-mid-blue hover:bg-cc-app-blue rounded-sm border border-white text-white", + "bg-cc-app-mid-blue hover:bg-cc-app-blue rounded-sm border border-white text-white hover:cursor-pointer", ccAppOutline: - "border-cc-app-mid-blue text-cc-app-blue hover:bg-cc-app-mid-blue/5 rounded-sm border focus:outline-none" + "border-cc-app-mid-blue text-cc-app-blue hover:bg-cc-app-mid-blue/5 rounded-sm border focus:outline-none hover:cursor-pointer", + ccAppOutlineRed: + "border-red text-white hover:bg-red-800 rounded-sm border focus:outline-none bg-red-700 hover:cursor-pointer" }, size: { md: "h-9 px-4 py-2 has-[>svg]:px-3", diff --git a/frontend/src/shared/components/ui/input.tsx b/frontend/src/shared/components/ui/input.tsx new file mode 100644 index 00000000..94d69579 --- /dev/null +++ b/frontend/src/shared/components/ui/input.tsx @@ -0,0 +1,21 @@ +import * as React from "react" + +import { cn } from "@/shared/utils/tailwindcss" + +function Input({ className, type, ...props }: React.ComponentProps<"input">) { + return ( + + ) +} + +export { Input } diff --git a/frontend/src/shared/components/ui/scroll-area.tsx b/frontend/src/shared/components/ui/scroll-area.tsx new file mode 100644 index 00000000..446e9a79 --- /dev/null +++ b/frontend/src/shared/components/ui/scroll-area.tsx @@ -0,0 +1,56 @@ +import * as React from "react" +import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" + +import { cn } from "@/shared/utils/tailwindcss" + +function ScrollArea({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + {children} + + + + + ) +} + +function ScrollBar({ + className, + orientation = "vertical", + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +export { ScrollArea, ScrollBar } diff --git a/frontend/src/shared/components/ui/select.tsx b/frontend/src/shared/components/ui/select.tsx new file mode 100644 index 00000000..e45ae7b1 --- /dev/null +++ b/frontend/src/shared/components/ui/select.tsx @@ -0,0 +1,183 @@ +import * as React from "react" +import * as SelectPrimitive from "@radix-ui/react-select" +import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react" + +import { cn } from "@/shared/utils/tailwindcss" + +function Select({ + ...props +}: React.ComponentProps) { + return +} + +function SelectGroup({ + ...props +}: React.ComponentProps) { + return +} + +function SelectValue({ + ...props +}: React.ComponentProps) { + return +} + +function SelectTrigger({ + className, + size = "default", + children, + ...props +}: React.ComponentProps & { + size?: "sm" | "default" +}) { + return ( + + {children} + + + + + ) +} + +function SelectContent({ + className, + children, + position = "popper", + ...props +}: React.ComponentProps) { + return ( + + + + + {children} + + + + + ) +} + +function SelectLabel({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function SelectItem({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function SelectSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function SelectScrollUpButton({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +function SelectScrollDownButton({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +export { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectScrollDownButton, + SelectScrollUpButton, + SelectSeparator, + SelectTrigger, + SelectValue, +} diff --git a/frontend/src/shared/constants/query-keys.ts b/frontend/src/shared/constants/query-keys.ts index f344fec4..eb1b5fcc 100644 --- a/frontend/src/shared/constants/query-keys.ts +++ b/frontend/src/shared/constants/query-keys.ts @@ -9,3 +9,7 @@ export const REPOSITORY_KEY="repository" export const REPOSITORY_CONTRIBUTORS_QUERY_KEY = "repository-contributors" export const REPOSITORY_LANGUAGES_QUERY_KEY = "repository-languages" export const REPOSITORY_ACTIVITIES_QUERY_KEY="repository-activites" +export const USER_ACTIVE_GOAL_LEVEL_QUERY_KEY="user-goal-level" +export const GOAL_LEVELS_QUERY_KEY="goal-levels" +export const USER_GOAL_LEVEL_PROGRESS_QUERY_KEY="goal-level-progresss" +export const CONTRIBUTION_TYPES_QUERY_KEY="contribution-types" \ No newline at end of file diff --git a/frontend/src/shared/layout/AdminLayout.tsx b/frontend/src/shared/layout/AdminLayout.tsx new file mode 100644 index 00000000..fd68c487 --- /dev/null +++ b/frontend/src/shared/layout/AdminLayout.tsx @@ -0,0 +1,66 @@ +import React, { type FC, type ReactNode } from "react"; +import { User, BarChart3, Users } from "lucide-react"; + +interface AdminLayoutProps { + children: ReactNode; +} + +const AdminLayout: FC = ({ children }) => { + const [activeItem, setActiveItem] = React.useState("Users"); + + const menuItems = [ + { name: "Configure Score", icon: BarChart3 }, + { name: "Users", icon: Users } + ]; + + return ( +
+
+
+
+
+ </> +
+ + CODE CURIOSITY + +
+
+ + +
+ +
+
+
{activeItem}
+
+ + Hi Admin +
+
+ +
{children}
+
+
+ ); +}; + +export default AdminLayout; diff --git a/frontend/src/shared/layout/AuthLayout.tsx b/frontend/src/shared/layout/AuthLayout.tsx index 083ac3a4..45407aed 100644 --- a/frontend/src/shared/layout/AuthLayout.tsx +++ b/frontend/src/shared/layout/AuthLayout.tsx @@ -1,5 +1,5 @@ import { type FC, type ReactNode, useEffect } from "react"; -import { useLocation, useNavigate } from "react-router-dom"; +import { Link, useLocation, useNavigate } from "react-router-dom"; import { CheckCircle } from "lucide-react"; import { Card } from "@/shared/components/ui/card"; @@ -70,9 +70,14 @@ const AuthLayout: FC = ({ children }) => {
- {children} + + Log in as admin? +
diff --git a/frontend/src/shared/types/types.ts b/frontend/src/shared/types/types.ts index c90e9b83..29db7501 100644 --- a/frontend/src/shared/types/types.ts +++ b/frontend/src/shared/types/types.ts @@ -114,3 +114,70 @@ export interface RepositoryActivity { createdAt: string; updatedAt: string; } + +export interface GoalLevel { + id: number; + level: string; + createdAt: string; + updatedAt: string; +} + +export interface GoalLevelProgress { + contributionType: string; + targetCount: number; + achievedCount: number; +} + +export interface CustomGoalLevelTarget { + contributionType: string; + target: number; +} + +export interface CustomGoalLevelTargetResponse { + id: number; + goalId: number; + contributionScoreId: number; + targetCount: number; + isCustom: boolean; + setByUserId: number; + createdAt: string; + updatedAt: string; +} + +export interface ContributionTypeDetail { + id: number; + adminId: number; + contributionType: string; + score: number; + createdAt: string; + updatedAt: string; +} + +export interface Admin { + userId: number; + githubId: number; + githubUsername: string; + email: string; + avatarUrl: string; + currentBalance: number; + currentActiveGoalId: { + Int64: number; + Valid: boolean; + }; + isBlocked: boolean; + isAdmin: boolean; + password: string; + isDeleted: boolean; + deletedAt: { + Time: string; + Valid: boolean; + }; + createdAt: string; + updatedAt: string; + jwtToken: string; +} + +export interface AdminCredentials { + email: string; + password: string; +} diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index 0e4ba5f0..f7c4e4d7 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -15,6 +15,7 @@ module.exports = { ccappblue: "hsl(var(--cc-app-blue))", ccappgraybackground: "hsl(var(--cc-app-gray-background))", ccapporange: "hsl(var(--cc-app-orange))", + ccappyellow: "hsl(var(--cc-app-yellow))", background: "hsl(var(--background))", foreground: "hsl(var(--foreground))", From 69a04af5c26b62b67ede9135c03ad39c8a0d8d69 Mon Sep 17 00:00:00 2001 From: VishakhaSainani-Josh Date: Tue, 5 Aug 2025 12:10:34 +0530 Subject: [PATCH 17/36] refactor login with github flow --- backend/internal/app/auth/handler.go | 13 ++----------- backend/internal/app/contribution/service.go | 6 ++++-- backend/internal/app/router.go | 2 +- backend/internal/pkg/middleware/middleware.go | 1 - 4 files changed, 7 insertions(+), 15 deletions(-) diff --git a/backend/internal/app/auth/handler.go b/backend/internal/app/auth/handler.go index e9091616..249a0527 100644 --- a/backend/internal/app/auth/handler.go +++ b/backend/internal/app/auth/handler.go @@ -2,7 +2,6 @@ package auth import ( "encoding/json" - "fmt" "log/slog" "net/http" @@ -47,19 +46,11 @@ func (h *handler) GithubOAuthLoginCallback(w http.ResponseWriter, r *http.Reques token, err := h.authService.GithubOAuthLoginCallback(ctx, code) if err != nil { slog.Error("failed to login with github", "error", err) - http.Redirect(w, r, fmt.Sprintf("%s?authError=%s", h.appConfig.ClientURL, LoginWithGithubFailed), http.StatusTemporaryRedirect) + response.WriteJson(w, http.StatusUnauthorized, "failed to log in with github", nil) return } - cookie := &http.Cookie{ - Name: AccessTokenCookieName, - Value: token, - //TODO set domain before deploying to production - // Domain: "yourdomain.com", - HttpOnly: true, - } - http.SetCookie(w, cookie) - http.Redirect(w, r, h.appConfig.ClientURL, http.StatusPermanentRedirect) + response.WriteJson(w, http.StatusOK, "successfully logged in with github", token) } func (h *handler) GetLoggedInUser(w http.ResponseWriter, r *http.Request) { diff --git a/backend/internal/app/contribution/service.go b/backend/internal/app/contribution/service.go index 2a658ed7..6a2d0631 100644 --- a/backend/internal/app/contribution/service.go +++ b/backend/internal/app/contribution/service.go @@ -22,6 +22,7 @@ const ( issueCommentEvent = "IssueCommentEvent" pullRequestCommentEvent = "PullRequestReviewCommentEvent" pullRequestReviewEvent = "PullRequestReviewEvent" + pushEvent = "PushEvent" ) // app contribution types @@ -34,6 +35,7 @@ const ( issueComment = "IssueComment" pullRequestComment = "PullRequestComment" pullRequestReviewed = "PullRequestReviewed" + CommitAdded = "CommitAdded" ) // payload @@ -200,8 +202,8 @@ func (s *service) GetContributionType(ctx context.Context, contribution Contribu contributionType = issueResolved } - // case pushEvent: - // contributionType = pullRequestUpdated + case pushEvent: + contributionType = CommitAdded case pullRequestReviewEvent: if action == PayloadCreatedKey || action == PayloadApprovedKey { diff --git a/backend/internal/app/router.go b/backend/internal/app/router.go index 2fc197b7..e8810972 100644 --- a/backend/internal/app/router.go +++ b/backend/internal/app/router.go @@ -17,7 +17,6 @@ func NewRouter(deps Dependencies) http.Handler { router.HandleFunc("GET /api/v1/auth/github", deps.AuthHandler.GithubOAuthLoginUrl) router.HandleFunc("GET /api/v1/auth/github/callback", deps.AuthHandler.GithubOAuthLoginCallback) router.HandleFunc("GET /api/v1/auth/user", middleware.Authentication(deps.AuthHandler.GetLoggedInUser, deps.AppCfg)) - router.HandleFunc("GET /api/v1/auth/admin", deps.AuthHandler.LoginAdmin) router.HandleFunc("PATCH /api/v1/user/email", middleware.Authentication(deps.UserHandler.UpdateUserEmail, deps.AppCfg)) router.HandleFunc("DELETE /api/v1/user/delete/{user_id}", middleware.Authentication(deps.UserHandler.SoftDeleteUser, deps.AppCfg)) @@ -43,6 +42,7 @@ func NewRouter(deps Dependencies) http.Handler { router.HandleFunc("GET /api/v1/user/badges", middleware.Authentication(deps.BadgeHandler.GetBadgeDetailsOfUser, deps.AppCfg)) + router.HandleFunc("POST /api/v1/auth/admin", deps.AuthHandler.LoginAdmin) router.HandleFunc("PATCH /api/v1/contributions/scores/configure", middleware.Authentication(middleware.AuthorizeAdmin(deps.ContributionHandler.ConfigureContributionTypeScore), deps.AppCfg)) router.HandleFunc("GET /api/v1/users", middleware.Authentication(middleware.AuthorizeAdmin(deps.UserHandler.ListAllUsers), deps.AppCfg)) router.HandleFunc("PATCH /api/v1/users/{user_id}", middleware.Authentication(middleware.AuthorizeAdmin(deps.UserHandler.BlockOrUnblockUser), deps.AppCfg)) diff --git a/backend/internal/pkg/middleware/middleware.go b/backend/internal/pkg/middleware/middleware.go index 3f0a9f3d..3682ee84 100644 --- a/backend/internal/pkg/middleware/middleware.go +++ b/backend/internal/pkg/middleware/middleware.go @@ -35,7 +35,6 @@ func ExtractTxFromContext(ctx context.Context) (*sqlx.Tx, bool) { func CorsMiddleware(next http.Handler, appCfg config.AppConfig) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", appCfg.ClientURL) - w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") From 795a8974b6f4e7a0d0332c5a3827c7d8b4500743 Mon Sep 17 00:00:00 2001 From: VishakhaSainani-Josh Date: Tue, 5 Aug 2025 14:49:32 +0530 Subject: [PATCH 18/36] implement admin panel --- frontend/package-lock.json | 1029 +++++++++++++++-- frontend/package.json | 1 + frontend/src/api/axios.ts | 14 +- frontend/src/api/queries/Admin.ts | 88 +- frontend/src/api/queries/Auth.ts | 22 + frontend/src/features/Admin/AdminLogin.tsx | 99 ++ .../src/features/Admin/ScoreConfigure.tsx | 93 ++ frontend/src/features/Admin/Users.tsx | 220 ++++ .../Login/components/AdminLoginComponent.tsx | 70 -- .../Login/components/LoginComponent.tsx | 30 +- frontend/src/main.tsx | 7 +- frontend/src/root/Router.tsx | 7 + frontend/src/root/routes-config.tsx | 28 +- frontend/src/shared/HOC/WithAuth.tsx | 37 + .../AdminDashboard/AdminProfile.tsx | 0 .../UserDashboard/UserProfileDetails.tsx | 9 +- frontend/src/shared/constants/layout.ts | 2 +- frontend/src/shared/constants/query-keys.ts | 3 +- frontend/src/shared/context/AuthProvider.tsx | 53 - frontend/src/shared/layout/AdminLayout.tsx | 131 ++- frontend/src/shared/layout/AuthLayout.tsx | 21 +- frontend/src/shared/types/types.ts | 14 + 22 files changed, 1719 insertions(+), 259 deletions(-) create mode 100644 frontend/src/api/queries/Auth.ts create mode 100644 frontend/src/features/Admin/AdminLogin.tsx create mode 100644 frontend/src/features/Admin/ScoreConfigure.tsx create mode 100644 frontend/src/features/Admin/Users.tsx delete mode 100644 frontend/src/features/Login/components/AdminLoginComponent.tsx create mode 100644 frontend/src/shared/components/AdminDashboard/AdminProfile.tsx delete mode 100644 frontend/src/shared/context/AuthProvider.tsx diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 8ead3f33..ca01eccb 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,7 +8,12 @@ "name": "code-curiosity-frontend", "version": "0.0.0", "dependencies": { + "@radix-ui/react-avatar": "^1.1.10", + "@radix-ui/react-dialog": "^1.1.14", + "@radix-ui/react-dropdown-menu": "^2.1.15", "@radix-ui/react-progress": "^1.1.7", + "@radix-ui/react-scroll-area": "^1.2.9", + "@radix-ui/react-select": "^2.2.5", "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slot": "^1.2.3", "@tailwindcss/vite": "^4.1.11", @@ -23,6 +28,7 @@ "next-themes": "^0.4.6", "react": "^19.1.0", "react-dom": "^19.1.0", + "react-hook-form": "^7.62.0", "react-router-dom": "^7.7.0", "sonner": "^2.0.6", "tailwind-merge": "^3.3.1", @@ -916,6 +922,44 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.3.tgz", + "integrity": "sha512-uZA413QEpNuhtb3/iIKoYMSK07keHPYeXF02Zhd6e213j+d1NamLix/mCLxBUDW/Gx52sPH2m+chlUsyaBs/Ag==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.5.tgz", + "integrity": "sha512-HDO/1/1oH9fjj4eLgegrlH3dklZpHtUYYFiVwMUwfGvk9jWDRWqkklA2/NFScknrcNSspbV868WjXORvreDX+Q==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.3" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -1036,55 +1080,706 @@ "dev": true, "license": "MIT", "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgr/core": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@radix-ui/number": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz", + "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.10.tgz", + "integrity": "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.14.tgz", + "integrity": "sha512-+CpweKjqpzTmwRwcYECQcNYbI8V9VSQt0SNFKeEBLgfucbsLssU6Ppq7wUdNXEGb573bMjFhVjKVll8rmV6zMw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.10", + "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.10.tgz", + "integrity": "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.15.tgz", + "integrity": "sha512-mIBnOjgwo9AH3FyKaSWoSu/dYj6VdhJ7frEPiGTeXCdUFHjl9h3mFh2wwhEtINOmYXWhdpf1rY2minFsmaNgVQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.15", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz", + "integrity": "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.15.tgz", + "integrity": "sha512-tVlmA3Vb9n8SZSd+YSbuFR66l87Wiy4du+YE+0hzKQEANA+7cWKH1WgqcEX4pXqxUFQKrWQGHdvEfw00TjFiew==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.10", + "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.7", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.10", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.7.tgz", + "integrity": "sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.4.tgz", + "integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.7.tgz", + "integrity": "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.10.tgz", + "integrity": "sha512-dT9aOXUen9JSsxnMPv/0VqySQf5eDQ6LCk5Sw28kamz8wSOW2bJdlX2Bg5VUIIcV+6XlHpWTIuTPCf/UNIyq8Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.9.tgz", + "integrity": "sha512-YSjEfBXnhUELsO2VzjdtYYD4CfQjvao+lhhrX5XsHD7/cyUNzljF1FHEbgTPN7LH2MClfwRMIsYlqTYpKTTe2A==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.5.tgz", + "integrity": "sha512-HnMTdXEVuuyzx63ME0ut4+sEMYW6oouHWNGUZc7ddvUWIcfCva/AMoqEW/3wnEllriMWBa0RHspCYnfCWJQYmA==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.10", + "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.7", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz", + "integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" }, - "engines": { - "node": ">= 8" + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", "license": "MIT", - "engines": { - "node": ">= 8" + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", "license": "MIT", "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" }, - "engines": { - "node": ">= 8" + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@pkgr/core": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", - "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", - "dev": true, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" }, - "funding": { - "url": "https://opencollective.com/pkgr" + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", - "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -1095,11 +1790,14 @@ } } }, - "node_modules/@radix-ui/react-context": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", - "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "node_modules/@radix-ui/react-use-is-hydrated": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz", + "integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==", "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.5.0" + }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -1110,94 +1808,101 @@ } } }, - "node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.2.3" - }, "peerDependencies": { "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { "optional": true - }, - "@types/react-dom": { + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { "optional": true } } }, - "node_modules/@radix-ui/react-progress": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.7.tgz", - "integrity": "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==", + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", "license": "MIT", "dependencies": { - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3" + "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { "optional": true - }, - "@types/react-dom": { - "optional": true } } }, - "node_modules/@radix-ui/react-separator": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz", - "integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==", + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.1.3" + "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { "optional": true - }, - "@types/react-dom": { - "optional": true } } }, - "node_modules/@radix-ui/react-slot": { + "node_modules/@radix-ui/react-visually-hidden": { "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", "license": "MIT", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" + "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { "optional": true + }, + "@types/react-dom": { + "optional": true } } }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.27", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", @@ -2259,6 +2964,18 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -2611,6 +3328,12 @@ "node": ">=8" } }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, "node_modules/dotenv": { "version": "17.2.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.1.tgz", @@ -3256,6 +3979,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", @@ -4513,6 +5245,22 @@ "react": "^19.1.0" } }, + "node_modules/react-hook-form": { + "version": "7.62.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.62.0.tgz", + "integrity": "sha512-7KWFejc98xqG/F4bAxpL41NB3o1nnvQO1RWZT3TqRZYL8RryQETGfEdVnJN2fy1crCiBLLjkRBVK05j24FxJGA==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, "node_modules/react-refresh": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", @@ -4523,6 +5271,53 @@ "node": ">=0.10.0" } }, + "node_modules/react-remove-scroll": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz", + "integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/react-router": { "version": "7.7.1", "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.7.1.tgz", @@ -4561,6 +5356,28 @@ "react-dom": ">=18" } }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -4981,6 +5798,12 @@ "typescript": ">=4.8.4" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/tw-animate-css": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.3.6.tgz", @@ -5090,6 +5913,58 @@ "punycode": "^2.1.0" } }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sync-external-store": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", + "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/vite": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/vite/-/vite-7.0.6.tgz", diff --git a/frontend/package.json b/frontend/package.json index 1bc19d05..2133d940 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -34,6 +34,7 @@ "next-themes": "^0.4.6", "react": "^19.1.0", "react-dom": "^19.1.0", + "react-hook-form": "^7.62.0", "react-router-dom": "^7.7.0", "sonner": "^2.0.6", "tailwind-merge": "^3.3.1", diff --git a/frontend/src/api/axios.ts b/frontend/src/api/axios.ts index 9ef6ebc8..418d7f3d 100644 --- a/frontend/src/api/axios.ts +++ b/frontend/src/api/axios.ts @@ -1,7 +1,8 @@ import axios from "axios"; import { BACKEND_URL } from "@/shared/constants/endpoints"; -import { getAccessToken } from "@/shared/utils/local-storage"; +import { clearAccessToken, getAccessToken } from "@/shared/utils/local-storage"; +import { LOGIN_PATH } from "@/shared/constants/routes"; export const api = axios.create({ baseURL: BACKEND_URL @@ -18,4 +19,15 @@ api.interceptors.request.use( error => Promise.reject(error) ); +api.interceptors.response.use( + response => response, + error => { + if (error.response?.status === 401) { + clearAccessToken(); + window.location.href = LOGIN_PATH; + } + return Promise.reject(error); + } +); + diff --git a/frontend/src/api/queries/Admin.ts b/frontend/src/api/queries/Admin.ts index 91441a3d..3b5f10d1 100644 --- a/frontend/src/api/queries/Admin.ts +++ b/frontend/src/api/queries/Admin.ts @@ -1,15 +1,20 @@ import { BACKEND_URL } from "@/shared/constants/endpoints"; import type { ApiResponse } from "@/shared/types/api"; import { api } from "../axios"; -import { useMutation } from "@tanstack/react-query"; -import type { AdminCredentials } from "@/shared/types/types"; +import { useMutation, useQuery } from "@tanstack/react-query"; +import type { + Admin, + AdminCredentials, + ContributionScore, + ContributionScoreUpdate +} from "@/shared/types/types"; const LogInAdmin = async ( adminCredentials: AdminCredentials -): Promise> => { - const response = await api.patch<{ +): Promise> => { + const response = await api.post<{ message: string; - data: null; + data: Admin; }>(`${BACKEND_URL}/api/v1/auth/admin`, { email: adminCredentials.email, password: adminCredentials.password @@ -24,3 +29,76 @@ export const useLogInAdmin = () => { LogInAdmin(adminCredentials) }); }; + +const getAllUsers = async (): Promise> => { + const response = await api.get<{ + message: string; + data: Admin[]; + }>(`${BACKEND_URL}/api/v1/users`); + + return response.data; +}; + +export const useGetAllUsers = () => { + return useQuery({ + queryKey: ["getAllUsers"], + queryFn: getAllUsers + }); +}; + +const updateUserBlockStatus = async ( + userId: number, + block: boolean +): Promise> => { + const response = await api.patch<{ + message: string; + data: null; + }>(`${BACKEND_URL}/api/v1/users/${userId}`, { + block + }); + + return response.data; +}; + +export const useUpdateUserBlockStatus = () => { + return useMutation({ + mutationFn: ({ userId, block }: { userId: number; block: boolean }) => + updateUserBlockStatus(userId, block) + }); +}; + +const fetchContributionTypes = async (): Promise< + ApiResponse +> => { + const response = await api.get<{ + message: string; + data: ContributionScore[]; + }>(`${BACKEND_URL}/api/v1/contributions/types`); + + return response.data; +}; + +export const useFetchContributionTypes = () => { + return useQuery({ + queryKey: ["fetch-contribution-types"], + queryFn: fetchContributionTypes + }); +}; + +const configureContributionScore = async ( + contributionScore: ContributionScoreUpdate[] +): Promise> => { + const response = await api.patch<{ + message: string; + data: ContributionScore[]; + }>(`${BACKEND_URL}/api/v1/contributions/scores/configure`, contributionScore); + + return response.data; +}; + +export const useConfigureContributionScore = () => { + return useMutation({ + mutationFn: (contributionScore: ContributionScoreUpdate[]) => + configureContributionScore(contributionScore) + }); +}; diff --git a/frontend/src/api/queries/Auth.ts b/frontend/src/api/queries/Auth.ts new file mode 100644 index 00000000..c2c7a18e --- /dev/null +++ b/frontend/src/api/queries/Auth.ts @@ -0,0 +1,22 @@ +import { BACKEND_URL } from "@/shared/constants/endpoints"; +import type { ApiResponse } from "@/shared/types/api"; +import { api } from "../axios"; +import { useQuery } from "@tanstack/react-query"; +import { GITHUB_OAUTH_LOGIN_QUERY_KEY } from "@/shared/constants/query-keys"; + +const githubOauthLogin = async (code: string): Promise> => { + const response = await api.get<{ + message: string; + data: string; + }>(`${BACKEND_URL}/api/v1/auth/github/callback?code=${code}`); + + return response.data; +}; + +export const useGithubOauthLogin = (code: string | null) => { + return useQuery({ + queryKey: [GITHUB_OAUTH_LOGIN_QUERY_KEY, code], + queryFn: () => githubOauthLogin(code!), + enabled: !!code + }); +}; diff --git a/frontend/src/features/Admin/AdminLogin.tsx b/frontend/src/features/Admin/AdminLogin.tsx new file mode 100644 index 00000000..24a22a38 --- /dev/null +++ b/frontend/src/features/Admin/AdminLogin.tsx @@ -0,0 +1,99 @@ +import { type FC } from "react"; +import type { AdminCredentials } from "@/shared/types/types"; +import { useLogInAdmin } from "@/api/queries/Admin"; +import { useForm } from "react-hook-form"; +import { useNavigate } from "react-router-dom"; +import { ACCESS_TOKEN_KEY } from "@/shared/constants/local-storage"; + +const AdminLogin: FC = () => { + const { + register, + handleSubmit, + formState: { errors } + } = useForm(); + + const navigate = useNavigate(); + + const { mutate, isPending, isError, error } = useLogInAdmin(); + const onSubmit = async ( + data: AdminCredentials, + event?: React.BaseSyntheticEvent + ) => { + try { + event?.preventDefault(); + console.log(" Submitting admin login", data); + + mutate(data, { + onSuccess: res => { + console.log(" Admin login success", res); + localStorage.setItem(ACCESS_TOKEN_KEY, res.data.jwtToken); + navigate("/admin/users"); + }, + onError: err => { + console.error(" Admin login error", err); + } + }); + } catch (err) { + console.error(" Unexpected submit error", err); + } + }; + + return ( +
+
+ + + {errors.email && ( +

{errors.email.message}

+ )} +
+ +
+ + + {errors.password && ( +

{errors.password.message}

+ )} +
+ + {isError && ( +

+ {(error as Error)?.message || "Failed to log in"} +

+ )} + + +
+ ); +}; + +export default AdminLogin; diff --git a/frontend/src/features/Admin/ScoreConfigure.tsx b/frontend/src/features/Admin/ScoreConfigure.tsx new file mode 100644 index 00000000..8f7a31b3 --- /dev/null +++ b/frontend/src/features/Admin/ScoreConfigure.tsx @@ -0,0 +1,93 @@ +import { useState } from "react"; + +import { Button } from "@/shared/components/ui/button"; +import { Input } from "@/shared/components/ui/input"; +import { Card } from "@/shared/components/ui/card"; +import { + useFetchContributionTypes, + useConfigureContributionScore +} from "@/api/queries/Admin"; +import type { + ContributionScore, + ContributionScoreUpdate +} from "@/shared/types/types"; +import { toast } from "sonner"; + +const ScoreConfigure = () => { + const { data, isLoading, isError } = useFetchContributionTypes(); + const { mutate: configureScore, isPending } = useConfigureContributionScore(); + + const [scores, setScores] = useState>({}); + + if (isLoading) return
Loading...
; + if (isError || !data?.data) + return
Failed to load contribution types.
; + + const handleScoreChange = (contributionType: string, newScore: number) => { + setScores(prev => ({ ...prev, [contributionType]: newScore })); + }; + + const handleSaveAll = () => { + const updates: ContributionScoreUpdate[] = Object.entries(scores).map( + ([contributionType, score]) => ({ + contributionType, + score + }) + ); + + if (updates.length === 0) { + toast.info("No changes to save."); + return; + } + + configureScore(updates, { + onSuccess: () => { + toast.success("Contribution scores updated successfully."); + setScores({}); + }, + onError: () => { + toast.error("Failed to update contribution scores."); + console.log(updates); + } + }); + }; + + return ( + +
+

+ Configure Contribution Scores +

+
+
+ {data.data.map((item: ContributionScore) => ( +
+
+ {item.contributionType.replace(/([a-z])([A-Z])/g, "$1 $2")} +
+ + + handleScoreChange(item.contributionType, Number(e.target.value)) + } + /> +
+ ))} +
+ +
+ +
+
+ ); +}; + +export default ScoreConfigure; diff --git a/frontend/src/features/Admin/Users.tsx b/frontend/src/features/Admin/Users.tsx new file mode 100644 index 00000000..9ae0e514 --- /dev/null +++ b/frontend/src/features/Admin/Users.tsx @@ -0,0 +1,220 @@ +import { type FC, useEffect, useState } from "react"; +import { Card } from "@/shared/components/ui/card"; +import defaultAvatar from "@/assets/default-profile-pic.svg"; +import { + Loader, + User, + Shield, + ShieldOff, + ChevronLeft, + ChevronRight +} from "lucide-react"; +import { useGetAllUsers, useUpdateUserBlockStatus } from "@/api/queries/Admin"; +import Coin from "@/shared/components/common/Coin"; +import { toast } from "sonner"; + +const ITEMS_PER_PAGE = 10; + +export const AllUsersList: FC = () => { + const { data, isLoading } = useGetAllUsers(); + const { mutate: updateBlockStatus } = useUpdateUserBlockStatus(); + + const [currentPage, setCurrentPage] = useState(1); + const [users, setUsers] = useState(data?.data || []); + + useEffect(() => { + if (data?.data) { + setUsers(data.data); + } + }, [data]); + + const totalPages = Math.ceil(users.length / ITEMS_PER_PAGE); + const startIndex = (currentPage - 1) * ITEMS_PER_PAGE; + const endIndex = startIndex + ITEMS_PER_PAGE; + const currentUsers = users.slice(startIndex, endIndex); + + const handleBlockToggle = (userId: number, block: boolean) => { + updateBlockStatus( + { userId, block }, + { + onSuccess: () => { + setUsers(prev => + prev.map(user => + user.userId === userId ? { ...user, isBlocked: block } : user + ) + ); + toast.success( + `User has been ${block ? "blocked" : "unblocked"} successfully.` + ); + }, + onError: () => { + toast.error("Something went wrong while updating block status."); + } + } + ); + }; + + const goToPage = (page: number) => { + setCurrentPage(Math.max(1, Math.min(page, totalPages))); + }; + + if (isLoading) { + return ( +
+ + Loading users... +
+ ); + } + + if (!users.length) { + return ( +
+
+ +

No users found

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

+ User Management +

+

+ Showing {startIndex + 1}-{Math.min(endIndex, users.length)} of{" "} + {users.length} users +

+
+ +
+ {currentUsers.map(user => ( + +
+
+
+ User Avatar +
+ +
+
+

+ {user.githubUsername} +

+
+ + {user.currentBalance} +
+
+
+ + {user.isBlocked ? ( + <> + + Blocked + + ) : ( + <> + + Active + + )} + +
+
+
+ +
+ +
+
+
+ ))} +
+ + {totalPages > 1 && ( +
+
+ + +
+ {Array.from({ length: Math.min(5, totalPages) }, (_, i) => { + let pageNum; + if (totalPages <= 5) { + pageNum = i + 1; + } else if (currentPage <= 3) { + pageNum = i + 1; + } else if (currentPage >= totalPages - 2) { + pageNum = totalPages - 4 + i; + } else { + pageNum = currentPage - 2 + i; + } + + return ( + + ); + })} +
+ + +
+ +
+ Page {currentPage} of {totalPages} +
+
+ )} +
+ ); +}; diff --git a/frontend/src/features/Login/components/AdminLoginComponent.tsx b/frontend/src/features/Login/components/AdminLoginComponent.tsx deleted file mode 100644 index cbc09b39..00000000 --- a/frontend/src/features/Login/components/AdminLoginComponent.tsx +++ /dev/null @@ -1,70 +0,0 @@ -// import { type FC } from "react"; -// import type { AdminCredentials } from "@/shared/types/types"; -// import { useLogInAdmin } from "@/api/queries/Admin"; - -// const AdminLogin: FC = () => { -// const { -// register, -// handleSubmit, -// formState: { errors }, -// } = useForm(); - -// const { mutate, isPending, isError, error } = useLogInAdmin(); - -// const onSubmit = (data: AdminCredentials) => { -// mutate(data); -// }; - -// return ( -//
-//
-// -// -// {errors.email && ( -//

{errors.email.message}

-// )} -//
- -//
-// -// -// {errors.password && ( -//

{errors.password.message}

-// )} -//
- -// {isError && ( -//

-// {(error as Error)?.message || "Failed to log in"} -//

-// )} - -// -//
-// ); -// }; - -// export default AdminLogin; diff --git a/frontend/src/features/Login/components/LoginComponent.tsx b/frontend/src/features/Login/components/LoginComponent.tsx index d5fd489f..2e0aba6c 100644 --- a/frontend/src/features/Login/components/LoginComponent.tsx +++ b/frontend/src/features/Login/components/LoginComponent.tsx @@ -7,15 +7,35 @@ import { } from "@/shared/components/ui/card"; import { GITHUB_AUTH_URL } from "@/shared/constants/endpoints"; import Coder from "@/assets/coder.svg"; -import { ACCESS_TOKEN_KEY } from "@/shared/constants/local-storage"; +import { useNavigate, useSearchParams } from "react-router-dom"; +import { useEffect } from "react"; +import { setAccessToken } from "@/shared/utils/local-storage"; +import { useGithubOauthLogin } from "@/api/queries/Auth"; +import { toast } from "sonner"; const LoginComponent = () => { + const [searchParams] = useSearchParams(); + const navigate = useNavigate(); + + const code = searchParams.get("code"); + const { data, isSuccess, isError } = useGithubOauthLogin(code); + + useEffect(() => { + if (!code) return; + + if (isSuccess && data?.data) { + const token = data.data; + setAccessToken(token); + navigate("/"); + } + + if (isError) { + toast.error("OAuth login failed:"); + } + }, [isSuccess, isError, data, navigate]); + const handleGithubLogin = () => { window.location.href = GITHUB_AUTH_URL || ""; - localStorage.setItem( - ACCESS_TOKEN_KEY, - "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjIsIklzQWRtaW4iOmZhbHNlLCJleHAiOjE3NTQzNzUwMDF9.FL-pl22Idc5Ge2iEiXTxYx5c1WBH06GZg5AYoonHiuI" - ); }; return ( diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 817fad21..cd015264 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -4,17 +4,14 @@ import { QueryClientProvider } from "@tanstack/react-query"; import { Toaster } from "sonner"; import Router from "@/root/Router"; -import { AuthProvider } from "@/shared/context/AuthProvider"; import { queryClient } from "@/api/react-query.ts"; import "./index.css"; createRoot(document.getElementById("root")!).render( - - - - + + ); diff --git a/frontend/src/root/Router.tsx b/frontend/src/root/Router.tsx index c6a6a359..a4773198 100644 --- a/frontend/src/root/Router.tsx +++ b/frontend/src/root/Router.tsx @@ -5,6 +5,7 @@ import { type RoutesType, routesConfig } from "@/root/routes-config"; import { Layout } from "@/shared/constants/layout"; import AuthLayout from "@/shared/layout/AuthLayout"; import UserDashboardLayout from "@/shared/layout/UserDashboardLayout"; +import AdminLayout from "@/shared/layout/AdminLayout"; const generateRoutes = (routes: RoutesType[]) => { return routes.map(({ path, element, isProtected, layout }) => { @@ -24,6 +25,12 @@ const generateRoutes = (routes: RoutesType[]) => { ); } + if(layout == Layout.AdminLayout) { + wrappedElement = ( + {wrappedElement} + ); + } + return { path, element: wrappedElement }; }); }; diff --git a/frontend/src/root/routes-config.tsx b/frontend/src/root/routes-config.tsx index 4b98220d..b1664b05 100644 --- a/frontend/src/root/routes-config.tsx +++ b/frontend/src/root/routes-config.tsx @@ -10,7 +10,9 @@ import { USER_DASHBOARD_PATH } from "@/shared/constants/routes"; import RepositoryDetails from "@/features/RepositoryDetails.tsx"; - +import AdminLogin from "@/features/Admin/AdminLogin"; +import { AllUsersList } from "@/features/Admin/Users.tsx"; +import ScoreConfigure from "@/features/Admin/ScoreConfigure"; export interface RoutesType { path: string; element: ReactNode; @@ -28,19 +30,37 @@ export const routesConfig: RoutesType[] = [ { path: USER_DASHBOARD_PATH, element: , - isProtected: false, + isProtected: true, layout: Layout.DashboardLayout }, { path: MY_CONTRIBUTIONS_PATH, element: , - isProtected: false, + isProtected: true, layout: Layout.DashboardLayout }, { path: REPOSITORY_DETAILS_PATH, element: , - isProtected: false, + isProtected: true, layout: Layout.DashboardLayout + }, + { + path: "/admin/login", + element: , + isProtected: false, + layout: Layout.AuthLayout + }, + { + path: "/admin/users", + element: , + isProtected: true, + layout: Layout.AdminLayout + }, + { + path: "/admin/configure/score", + element: , + isProtected: true, + layout: Layout.AdminLayout } ]; diff --git a/frontend/src/shared/HOC/WithAuth.tsx b/frontend/src/shared/HOC/WithAuth.tsx index fb44e44a..fd3cc14f 100644 --- a/frontend/src/shared/HOC/WithAuth.tsx +++ b/frontend/src/shared/HOC/WithAuth.tsx @@ -27,3 +27,40 @@ const WithAuth: FC = ({ children }) => { }; export default WithAuth; + + + + +// import { useEffect } from 'react' +// import { useNavigate, useSearchParams } from 'react-router-dom' + +// export default function AuthCallback() { +// const [searchParams] = useSearchParams() +// const navigate = useNavigate() + +// useEffect(() => { +// const code = searchParams.get('code') +// if (!code) return + +// // Call backend to exchange code for token +// fetch(`http://localhost:8080/github/callback?code=${code}`) +// .then(res => res.json()) +// .then(data => { +// const token = data.accessToken +// if (token) { +// // Store token in localStorage (or sessionStorage) +// localStorage.setItem('accessToken', token) + +// // Navigate to home or dashboard +// navigate('/dashboard') +// } else { +// console.error('No token received') +// } +// }) +// .catch(err => { +// console.error('GitHub login failed:', err) +// }) +// }, []) + +// return
Logging in...
+// } \ No newline at end of file diff --git a/frontend/src/shared/components/AdminDashboard/AdminProfile.tsx b/frontend/src/shared/components/AdminDashboard/AdminProfile.tsx new file mode 100644 index 00000000..e69de29b diff --git a/frontend/src/shared/components/UserDashboard/UserProfileDetails.tsx b/frontend/src/shared/components/UserDashboard/UserProfileDetails.tsx index c105babd..2466e7c2 100644 --- a/frontend/src/shared/components/UserDashboard/UserProfileDetails.tsx +++ b/frontend/src/shared/components/UserDashboard/UserProfileDetails.tsx @@ -6,13 +6,15 @@ import { Button } from "@/shared/components/ui/button"; import DefaultProfilePic from "@/assets/default-profile-pic.svg"; import { Separator } from "@/shared/components/ui/separator"; import { useLoggedInUser } from "@/api/queries/UserProfileDetails"; -import { Link } from "react-router-dom"; +import { Link, useNavigate } from "react-router-dom"; import UserProfileMenu from "./UserProfileMenu"; import UserEmail from "./UserEmail"; import SettingsDialog from "./SettingsDialog"; +import { clearAccessToken } from "@/shared/utils/local-storage"; const UserProfileDetails = () => { + const navigate = useNavigate(); const { data } = useLoggedInUser(); const user = data?.data; @@ -20,7 +22,10 @@ const UserProfileDetails = () => { const [showEmailDialog, setShowEmailDialog] = useState(false); const handleSettingsClick = () => setShowSettingsDialog(true); - const handleLogoutClick = () => console.log("Logout clicked"); + const handleLogoutClick = () => { + clearAccessToken(); + navigate("/login"); + }; const handleDeleteAccount = () => { console.log("Delete account clicked"); diff --git a/frontend/src/shared/constants/layout.ts b/frontend/src/shared/constants/layout.ts index 9be0dcda..7b59a8d1 100644 --- a/frontend/src/shared/constants/layout.ts +++ b/frontend/src/shared/constants/layout.ts @@ -1,8 +1,8 @@ export const Layout = { AuthLayout: "AuthLayout", DashboardLayout: "DashboardLayout", + AdminLayout: "AdminLayout", None: "None" } as const; export type LayoutType = (typeof Layout)[keyof typeof Layout]; - diff --git a/frontend/src/shared/constants/query-keys.ts b/frontend/src/shared/constants/query-keys.ts index eb1b5fcc..9f5cf685 100644 --- a/frontend/src/shared/constants/query-keys.ts +++ b/frontend/src/shared/constants/query-keys.ts @@ -12,4 +12,5 @@ export const REPOSITORY_ACTIVITIES_QUERY_KEY="repository-activites" export const USER_ACTIVE_GOAL_LEVEL_QUERY_KEY="user-goal-level" export const GOAL_LEVELS_QUERY_KEY="goal-levels" export const USER_GOAL_LEVEL_PROGRESS_QUERY_KEY="goal-level-progresss" -export const CONTRIBUTION_TYPES_QUERY_KEY="contribution-types" \ No newline at end of file +export const CONTRIBUTION_TYPES_QUERY_KEY="contribution-types" +export const GITHUB_OAUTH_LOGIN_QUERY_KEY="github-oauth-login" \ No newline at end of file diff --git a/frontend/src/shared/context/AuthProvider.tsx b/frontend/src/shared/context/AuthProvider.tsx deleted file mode 100644 index a5e1340f..00000000 --- a/frontend/src/shared/context/AuthProvider.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { type ReactNode, createContext, useMemo, useState } from "react"; - -import { clearAccessToken, setAccessToken } from "@/shared/utils/local-storage"; - -export type UserCredentials = { - githubId: string; - githubUsername: string; - avatarUrl: string; -}; - -export interface AuthContextInterface { - userCredentials: UserCredentials | null; - login: (userCredentials: UserCredentials, token: string) => void; - logout: () => void; -} - -const AuthContext = createContext({ - userCredentials: null, - login: () => { - throw new Error("AuthContext: login called outside AuthProvider"); - }, - logout: () => { - throw new Error("AuthContext: logout called outside AuthProvider"); - } -}); - -type AuthProviderProps = { - children: ReactNode; -}; - -export const AuthProvider = ({ children }: AuthProviderProps) => { - const [userCredentials, setUserCredentials] = - useState(null); - - const login = (userCredentials: UserCredentials, token: string) => { - setUserCredentials(userCredentials); - setAccessToken(token); - }; - - const logout = () => { - setUserCredentials(null); - clearAccessToken(); - }; - - const value = useMemo( - () => ({ userCredentials, login, logout }), - [userCredentials] - ); - - return {children}; -}; - -export { AuthContext }; diff --git a/frontend/src/shared/layout/AdminLayout.tsx b/frontend/src/shared/layout/AdminLayout.tsx index fd68c487..41ba296d 100644 --- a/frontend/src/shared/layout/AdminLayout.tsx +++ b/frontend/src/shared/layout/AdminLayout.tsx @@ -1,5 +1,6 @@ import React, { type FC, type ReactNode } from "react"; -import { User, BarChart3, Users } from "lucide-react"; +import { User, BarChart3, Users, ChevronRight } from "lucide-react"; +import { useNavigate } from "react-router-dom"; interface AdminLayoutProps { children: ReactNode; @@ -7,57 +8,129 @@ interface AdminLayoutProps { const AdminLayout: FC = ({ children }) => { const [activeItem, setActiveItem] = React.useState("Users"); + const navigate = useNavigate(); const menuItems = [ - { name: "Configure Score", icon: BarChart3 }, - { name: "Users", icon: Users } + { name: "Configure Score", icon: BarChart3, path: "/admin/configure/score" }, + { name: "Users", icon: Users, path: "/admin/users" } ]; + const handleMenuClick = (itemName: string, path: string) => { + setActiveItem(itemName); + navigate(path); + }; + return ( -
-
-
-
-
- </> +
+
+
+
+
+ {Array.from({ length: 24 }).map((_, i) => ( +
+ ))} +
+
+ +
+
+ </> +
+
+ + CODE CURIOSITY + + + Admin Portal +
- - CODE CURIOSITY -
-
-
-
-
{activeItem}
-
- - Hi Admin +
+
+ {Array.from({ length: 18 }).map((_, i) => ( +
+ ))}
+
-
{children}
+
+
+
+
+
+

+ {activeItem} +

+

+ Manage your {activeItem.toLowerCase()} settings and configurations +

+
+
+ +
+
+
+ +
+
+ Admin + Administrator +
+
+
+
+
+ +
+
+ {children} +
+
); diff --git a/frontend/src/shared/layout/AuthLayout.tsx b/frontend/src/shared/layout/AuthLayout.tsx index 45407aed..cc1bdee0 100644 --- a/frontend/src/shared/layout/AuthLayout.tsx +++ b/frontend/src/shared/layout/AuthLayout.tsx @@ -72,12 +72,21 @@ const AuthLayout: FC = ({ children }) => { {children} - - Log in as admin? - + {location.pathname.startsWith("/admin") ? ( + + Log in as user? + + ) : ( + + Log in as admin? + + )}
diff --git a/frontend/src/shared/types/types.ts b/frontend/src/shared/types/types.ts index 29db7501..75ad6331 100644 --- a/frontend/src/shared/types/types.ts +++ b/frontend/src/shared/types/types.ts @@ -181,3 +181,17 @@ export interface AdminCredentials { email: string; password: string; } + +export interface ContributionScore { + id: number; + adminId: number; + contributionType: string; + score: number; + createdAt: string; + updatedAt: string; +} + +export interface ContributionScoreUpdate { + contributionType: string; + score: number; +} From c71f1111da4c7294ed9a043a5ec9b623b6178611 Mon Sep 17 00:00:00 2001 From: VishakhaSainani-Josh Date: Wed, 6 Aug 2025 14:08:47 +0530 Subject: [PATCH 19/36] implement admin features --- frontend/src/api/queries/Admin.ts | 8 +- .../src/api/queries/UserProfileDetails.ts | 6 +- frontend/src/assets/github-white-icon.svg | 1 + frontend/src/features/Admin/AdminLogin.tsx | 19 +- .../src/features/Admin/ScoreConfigure.tsx | 5 +- .../Login/components/LoginComponent.tsx | 17 +- frontend/src/root/routes-config.tsx | 17 +- frontend/src/shared/HOC/WithAuth.tsx | 39 +--- .../UserDashboard/DeleteAccount.tsx | 5 + .../UserDashboard/UserProfileDetails.tsx | 5 - .../components/common/BlockedAccountPage.tsx | 52 +++++ frontend/src/shared/constants/constants.ts | 10 +- frontend/src/shared/constants/query-keys.ts | 34 ++-- frontend/src/shared/constants/routes.ts | 6 +- frontend/src/shared/layout/AdminLayout.tsx | 188 ++++++++++-------- 15 files changed, 242 insertions(+), 170 deletions(-) create mode 100644 frontend/src/assets/github-white-icon.svg create mode 100644 frontend/src/shared/components/common/BlockedAccountPage.tsx diff --git a/frontend/src/api/queries/Admin.ts b/frontend/src/api/queries/Admin.ts index 3b5f10d1..ed6e2dd6 100644 --- a/frontend/src/api/queries/Admin.ts +++ b/frontend/src/api/queries/Admin.ts @@ -8,6 +8,10 @@ import type { ContributionScore, ContributionScoreUpdate } from "@/shared/types/types"; +import { + CONTRIBUTION_TYPES_QUERY_KEY, + GET_ALL_USERS_QUERY_KEY +} from "@/shared/constants/query-keys"; const LogInAdmin = async ( adminCredentials: AdminCredentials @@ -41,7 +45,7 @@ const getAllUsers = async (): Promise> => { export const useGetAllUsers = () => { return useQuery({ - queryKey: ["getAllUsers"], + queryKey: [GET_ALL_USERS_QUERY_KEY], queryFn: getAllUsers }); }; @@ -80,7 +84,7 @@ const fetchContributionTypes = async (): Promise< export const useFetchContributionTypes = () => { return useQuery({ - queryKey: ["fetch-contribution-types"], + queryKey: [CONTRIBUTION_TYPES_QUERY_KEY], queryFn: fetchContributionTypes }); }; diff --git a/frontend/src/api/queries/UserProfileDetails.ts b/frontend/src/api/queries/UserProfileDetails.ts index 9811fa4c..fba68ec3 100644 --- a/frontend/src/api/queries/UserProfileDetails.ts +++ b/frontend/src/api/queries/UserProfileDetails.ts @@ -14,10 +14,12 @@ const fetchLoggedInUser = async (): Promise> => { return response.data; }; -export const useLoggedInUser = () => { +export const useLoggedInUser = (enabled: boolean = true) => { return useQuery({ queryKey: [LOGGED_IN_USER_QUERY_KEY], - queryFn: fetchLoggedInUser + queryFn: fetchLoggedInUser, + enabled, + retry: false }); }; diff --git a/frontend/src/assets/github-white-icon.svg b/frontend/src/assets/github-white-icon.svg new file mode 100644 index 00000000..309fecd8 --- /dev/null +++ b/frontend/src/assets/github-white-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/features/Admin/AdminLogin.tsx b/frontend/src/features/Admin/AdminLogin.tsx index 24a22a38..164b8036 100644 --- a/frontend/src/features/Admin/AdminLogin.tsx +++ b/frontend/src/features/Admin/AdminLogin.tsx @@ -4,6 +4,7 @@ import { useLogInAdmin } from "@/api/queries/Admin"; import { useForm } from "react-hook-form"; import { useNavigate } from "react-router-dom"; import { ACCESS_TOKEN_KEY } from "@/shared/constants/local-storage"; +import { Button } from "@/shared/components/ui/button"; const AdminLogin: FC = () => { const { @@ -11,10 +12,9 @@ const AdminLogin: FC = () => { handleSubmit, formState: { errors } } = useForm(); - const navigate = useNavigate(); - const { mutate, isPending, isError, error } = useLogInAdmin(); + const onSubmit = async ( data: AdminCredentials, event?: React.BaseSyntheticEvent @@ -22,7 +22,6 @@ const AdminLogin: FC = () => { try { event?.preventDefault(); console.log(" Submitting admin login", data); - mutate(data, { onSuccess: res => { console.log(" Admin login success", res); @@ -53,7 +52,7 @@ const AdminLogin: FC = () => { {errors.email && ( @@ -71,7 +70,7 @@ const AdminLogin: FC = () => { {errors.password && ( @@ -81,19 +80,19 @@ const AdminLogin: FC = () => { {isError && (

- {(error as Error)?.message || "Failed to log in"} + {error?.message || "Failed to log in"}

)} - + ); }; -export default AdminLogin; +export default AdminLogin; \ No newline at end of file diff --git a/frontend/src/features/Admin/ScoreConfigure.tsx b/frontend/src/features/Admin/ScoreConfigure.tsx index 8f7a31b3..9976c8e6 100644 --- a/frontend/src/features/Admin/ScoreConfigure.tsx +++ b/frontend/src/features/Admin/ScoreConfigure.tsx @@ -2,7 +2,6 @@ import { useState } from "react"; import { Button } from "@/shared/components/ui/button"; import { Input } from "@/shared/components/ui/input"; -import { Card } from "@/shared/components/ui/card"; import { useFetchContributionTypes, useConfigureContributionScore @@ -53,7 +52,7 @@ const ScoreConfigure = () => { }; return ( - +

Configure Contribution Scores @@ -86,7 +85,7 @@ const ScoreConfigure = () => { {isPending ? "Saving..." : "Save All"}

- +
); }; diff --git a/frontend/src/features/Login/components/LoginComponent.tsx b/frontend/src/features/Login/components/LoginComponent.tsx index 2e0aba6c..bd035452 100644 --- a/frontend/src/features/Login/components/LoginComponent.tsx +++ b/frontend/src/features/Login/components/LoginComponent.tsx @@ -12,7 +12,9 @@ import { useEffect } from "react"; import { setAccessToken } from "@/shared/utils/local-storage"; import { useGithubOauthLogin } from "@/api/queries/Auth"; import { toast } from "sonner"; - +import { useLoggedInUser } from "@/api/queries/UserProfileDetails"; +import { ACCOUNT_INFO_PATH } from "@/shared/constants/routes"; +import githubIcon from "@/assets/github-white-icon.svg"; const LoginComponent = () => { const [searchParams] = useSearchParams(); const navigate = useNavigate(); @@ -20,12 +22,21 @@ const LoginComponent = () => { const code = searchParams.get("code"); const { data, isSuccess, isError } = useGithubOauthLogin(code); + const { refetch } = useLoggedInUser(false); + useEffect(() => { if (!code) return; if (isSuccess && data?.data) { const token = data.data; setAccessToken(token); + + refetch().then(({ data: userData }) => { + if (userData?.data?.isBlocked) { + navigate(ACCOUNT_INFO_PATH); + } + }); + navigate("/"); } @@ -47,9 +58,9 @@ const LoginComponent = () => {
diff --git a/frontend/src/root/routes-config.tsx b/frontend/src/root/routes-config.tsx index b1664b05..a6726725 100644 --- a/frontend/src/root/routes-config.tsx +++ b/frontend/src/root/routes-config.tsx @@ -4,6 +4,10 @@ import Login from "@/features/Login"; import MyContributions from "@/features/MyContributions"; import UserDashboard from "@/features/UserDashboard"; import { + ACCOUNT_INFO_PATH, + ADMIN_LOGIN_PATH, + ADMIN_SCORE_CONFIGURE_PATH, + ADMIN_USERS_PATH, LOGIN_PATH, MY_CONTRIBUTIONS_PATH, REPOSITORY_DETAILS_PATH, @@ -13,6 +17,7 @@ import RepositoryDetails from "@/features/RepositoryDetails.tsx"; import AdminLogin from "@/features/Admin/AdminLogin"; import { AllUsersList } from "@/features/Admin/Users.tsx"; import ScoreConfigure from "@/features/Admin/ScoreConfigure"; +import BlockedAccountPage from "@/shared/components/common/BlockedAccountPage"; export interface RoutesType { path: string; element: ReactNode; @@ -46,21 +51,27 @@ export const routesConfig: RoutesType[] = [ layout: Layout.DashboardLayout }, { - path: "/admin/login", + path: ADMIN_LOGIN_PATH, element: , isProtected: false, layout: Layout.AuthLayout }, { - path: "/admin/users", + path: ADMIN_USERS_PATH, element: , isProtected: true, layout: Layout.AdminLayout }, { - path: "/admin/configure/score", + path: ADMIN_SCORE_CONFIGURE_PATH, element: , isProtected: true, layout: Layout.AdminLayout + }, + { + path: ACCOUNT_INFO_PATH, + element: , + isProtected: false, + layout: Layout.None } ]; diff --git a/frontend/src/shared/HOC/WithAuth.tsx b/frontend/src/shared/HOC/WithAuth.tsx index fd3cc14f..171ba7e9 100644 --- a/frontend/src/shared/HOC/WithAuth.tsx +++ b/frontend/src/shared/HOC/WithAuth.tsx @@ -26,41 +26,4 @@ const WithAuth: FC = ({ children }) => { return <>{children}; }; -export default WithAuth; - - - - -// import { useEffect } from 'react' -// import { useNavigate, useSearchParams } from 'react-router-dom' - -// export default function AuthCallback() { -// const [searchParams] = useSearchParams() -// const navigate = useNavigate() - -// useEffect(() => { -// const code = searchParams.get('code') -// if (!code) return - -// // Call backend to exchange code for token -// fetch(`http://localhost:8080/github/callback?code=${code}`) -// .then(res => res.json()) -// .then(data => { -// const token = data.accessToken -// if (token) { -// // Store token in localStorage (or sessionStorage) -// localStorage.setItem('accessToken', token) - -// // Navigate to home or dashboard -// navigate('/dashboard') -// } else { -// console.error('No token received') -// } -// }) -// .catch(err => { -// console.error('GitHub login failed:', err) -// }) -// }, []) - -// return
Logging in...
-// } \ No newline at end of file +export default WithAuth; \ No newline at end of file diff --git a/frontend/src/shared/components/UserDashboard/DeleteAccount.tsx b/frontend/src/shared/components/UserDashboard/DeleteAccount.tsx index c3d114cc..8ab0c35d 100644 --- a/frontend/src/shared/components/UserDashboard/DeleteAccount.tsx +++ b/frontend/src/shared/components/UserDashboard/DeleteAccount.tsx @@ -11,6 +11,8 @@ import { useLoggedInUser, useSoftDeleteUser } from "@/api/queries/UserProfileDetails"; +import { clearAccessToken } from "@/shared/utils/local-storage"; +import { useNavigate } from "react-router-dom"; interface Props { open: boolean; @@ -18,6 +20,7 @@ interface Props { } const DeleteAccount = ({ open, onClose }: Props) => { + const navigate = useNavigate(); const { data } = useLoggedInUser(); const user = data?.data; const { mutate: softDeleteUser, isPending } = useSoftDeleteUser(); @@ -27,6 +30,8 @@ const DeleteAccount = ({ open, onClose }: Props) => { softDeleteUser(user.userId, { onSuccess: () => { onClose(); + clearAccessToken(); + navigate("/login"); } }); }; diff --git a/frontend/src/shared/components/UserDashboard/UserProfileDetails.tsx b/frontend/src/shared/components/UserDashboard/UserProfileDetails.tsx index 2466e7c2..f3414c5d 100644 --- a/frontend/src/shared/components/UserDashboard/UserProfileDetails.tsx +++ b/frontend/src/shared/components/UserDashboard/UserProfileDetails.tsx @@ -27,11 +27,6 @@ const UserProfileDetails = () => { navigate("/login"); }; - const handleDeleteAccount = () => { - console.log("Delete account clicked"); - setShowSettingsDialog(false); - }; - const handleUpdateEmail = () => { setShowSettingsDialog(false); setShowEmailDialog(true); diff --git a/frontend/src/shared/components/common/BlockedAccountPage.tsx b/frontend/src/shared/components/common/BlockedAccountPage.tsx new file mode 100644 index 00000000..6ee86350 --- /dev/null +++ b/frontend/src/shared/components/common/BlockedAccountPage.tsx @@ -0,0 +1,52 @@ +import { + Card, + CardContent, + CardHeader, + CardFooter +} from "@/shared/components/ui/card"; +import { Button } from "@/shared/components/ui/button"; +import { clearAccessToken } from "@/shared/utils/local-storage"; +import { useNavigate } from "react-router-dom"; +import { AlertTriangle } from "lucide-react"; + +const BlockedAccountPage = () => { + const navigate = useNavigate(); + clearAccessToken(); + + const handleBackToLogin = () => { + clearAccessToken(); + navigate("/login"); + }; + + return ( +
+ + + +

+ Account Blocked +

+
+ + +

+ Your account has been blocked due to policy violations or unusual + activity. If you believe this is a mistake, please contact our + support team. +

+
+ + + + +
+
+ ); +}; + +export default BlockedAccountPage; diff --git a/frontend/src/shared/constants/constants.ts b/frontend/src/shared/constants/constants.ts index 862fd73e..ecf83ebf 100644 --- a/frontend/src/shared/constants/constants.ts +++ b/frontend/src/shared/constants/constants.ts @@ -1,8 +1,10 @@ export const AuthLayoutDetails = [ - "Earn and Upskill", - "Set Your Goals", - "Leader Board", - "Open Source Contribution" + "Fuel your open-source journey — stay consistent, stay rewarded.", + "Every commit, issue & comment earns real rewards (Virtual money).", + "Beginner-friendly — even small contributions count.", + "Set monthly goals and track your GitHub activity automatically.", + "Join a community driven by passion, not ads or data sales.", + "Supported by developers. Funded by developers. Built for developers." ]; export const LangColor: Record = { diff --git a/frontend/src/shared/constants/query-keys.ts b/frontend/src/shared/constants/query-keys.ts index 9f5cf685..5789c00e 100644 --- a/frontend/src/shared/constants/query-keys.ts +++ b/frontend/src/shared/constants/query-keys.ts @@ -1,16 +1,18 @@ -export const LOGGED_IN_USER_QUERY_KEY = "logged-in-user" -export const USER_BADGES_QUERY_KEY = "user-badges" -export const LEADERBOARD_QUERY_KEY="leaderboard" -export const CURRENT_USER_RANK_QUERY_KEY="current-user-rank" -export const RECENT_ACTIVITIES_QUERY_KEY="recent-activities" -export const OVERVIEW_QUERY_KEY="overview" -export const REPOSITORIES_KEY="repositories" -export const REPOSITORY_KEY="repository" -export const REPOSITORY_CONTRIBUTORS_QUERY_KEY = "repository-contributors" -export const REPOSITORY_LANGUAGES_QUERY_KEY = "repository-languages" -export const REPOSITORY_ACTIVITIES_QUERY_KEY="repository-activites" -export const USER_ACTIVE_GOAL_LEVEL_QUERY_KEY="user-goal-level" -export const GOAL_LEVELS_QUERY_KEY="goal-levels" -export const USER_GOAL_LEVEL_PROGRESS_QUERY_KEY="goal-level-progresss" -export const CONTRIBUTION_TYPES_QUERY_KEY="contribution-types" -export const GITHUB_OAUTH_LOGIN_QUERY_KEY="github-oauth-login" \ No newline at end of file +export const LOGGED_IN_USER_QUERY_KEY = "logged-in-user"; +export const USER_BADGES_QUERY_KEY = "user-badges"; +export const LEADERBOARD_QUERY_KEY = "leaderboard"; +export const CURRENT_USER_RANK_QUERY_KEY = "current-user-rank"; +export const RECENT_ACTIVITIES_QUERY_KEY = "recent-activities"; +export const OVERVIEW_QUERY_KEY = "overview"; +export const REPOSITORIES_KEY = "repositories"; +export const REPOSITORY_KEY = "repository"; +export const REPOSITORY_CONTRIBUTORS_QUERY_KEY = "repository-contributors"; +export const REPOSITORY_LANGUAGES_QUERY_KEY = "repository-languages"; +export const REPOSITORY_ACTIVITIES_QUERY_KEY = "repository-activites"; +export const USER_ACTIVE_GOAL_LEVEL_QUERY_KEY = "user-goal-level"; +export const GOAL_LEVELS_QUERY_KEY = "goal-levels"; +export const USER_GOAL_LEVEL_PROGRESS_QUERY_KEY = "goal-level-progresss"; +export const CONTRIBUTION_TYPES_QUERY_KEY = "contribution-types"; +export const GITHUB_OAUTH_LOGIN_QUERY_KEY = "github-oauth-login"; + +export const GET_ALL_USERS_QUERY_KEY = "get-all-users"; diff --git a/frontend/src/shared/constants/routes.ts b/frontend/src/shared/constants/routes.ts index 3294ebf7..7dc7b3b6 100644 --- a/frontend/src/shared/constants/routes.ts +++ b/frontend/src/shared/constants/routes.ts @@ -1,5 +1,9 @@ export const LOGIN_PATH = "/login"; - export const USER_DASHBOARD_PATH = "/"; export const MY_CONTRIBUTIONS_PATH = "/my-contributions"; export const REPOSITORY_DETAILS_PATH = "/repositories/:repoid"; +export const ACCOUNT_INFO_PATH = "/account/info"; + +export const ADMIN_LOGIN_PATH = "/admin/login"; +export const ADMIN_USERS_PATH = "/admin/users"; +export const ADMIN_SCORE_CONFIGURE_PATH = "/admin/configure/score"; diff --git a/frontend/src/shared/layout/AdminLayout.tsx b/frontend/src/shared/layout/AdminLayout.tsx index 41ba296d..98ff8f87 100644 --- a/frontend/src/shared/layout/AdminLayout.tsx +++ b/frontend/src/shared/layout/AdminLayout.tsx @@ -1,6 +1,9 @@ import React, { type FC, type ReactNode } from "react"; -import { User, BarChart3, Users, ChevronRight } from "lucide-react"; +import { User, BarChart3, Users, ChevronRight, LogOut } from "lucide-react"; import { useNavigate } from "react-router-dom"; +import { ADMIN_LOGIN_PATH } from "../constants/routes"; +import { Button } from "../components/ui/button"; +import { clearAccessToken } from "../utils/local-storage"; interface AdminLayoutProps { children: ReactNode; @@ -11,7 +14,11 @@ const AdminLayout: FC = ({ children }) => { const navigate = useNavigate(); const menuItems = [ - { name: "Configure Score", icon: BarChart3, path: "/admin/configure/score" }, + { + name: "Configure Score", + icon: BarChart3, + path: "/admin/configure/score" + }, { name: "Users", icon: Users, path: "/admin/users" } ]; @@ -21,104 +28,121 @@ const AdminLayout: FC = ({ children }) => { }; return ( -
-
-
-
-
- {Array.from({ length: 24 }).map((_, i) => ( -
- ))} -
-
- -
-
- </> +
+
+
+
+
+
+ {Array.from({ length: 24 }).map((_, i) => ( +
+ ))} +
-
- - CODE CURIOSITY - - - Admin Portal - + +
+
+ </> +
+
+ + CODE CURIOSITY + + + Admin Portal + +
-
-
- - - - {isActive && ( -
- )} - - ); - })} - -
-
- {Array.from({ length: 18 }).map((_, i) => ( -
- ))} -
+ + + {isActive && ( +
+ )} + + ); + })} + +
+ +
+
-
-
+
+
-

+

{activeItem}

-

- Manage your {activeItem.toLowerCase()} settings and configurations +

+ Manage your {activeItem.toLowerCase()} settings and + configurations

- +
-
-
+
+
- Admin + + Admin + Administrator
@@ -126,10 +150,8 @@ const AdminLayout: FC = ({ children }) => {
-
-
- {children} -
+
+
{children}
From 218be4c09108c75460cd2955930011aa5215c95e Mon Sep 17 00:00:00 2001 From: VishakhaSainani-Josh Date: Wed, 6 Aug 2025 14:09:53 +0530 Subject: [PATCH 20/36] rewire main.go into a cli app --- backend/cmd/main.go | 76 +++++++++++++++++++++++++++++++--- backend/go.mod | 11 ++++- backend/go.sum | 20 +++++++++ backend/internal/config/app.go | 1 + backend/internal/db/migrate.go | 2 +- 5 files changed, 102 insertions(+), 8 deletions(-) diff --git a/backend/cmd/main.go b/backend/cmd/main.go index c0fc99e0..5ddb70ed 100644 --- a/backend/cmd/main.go +++ b/backend/cmd/main.go @@ -14,28 +14,84 @@ import ( "github.com/joshsoftware/code-curiosity-2025/internal/app" "github.com/joshsoftware/code-curiosity-2025/internal/app/cronJob" "github.com/joshsoftware/code-curiosity-2025/internal/config" + "github.com/joshsoftware/code-curiosity-2025/internal/db" + "github.com/rs/cors" + "github.com/urfave/cli" ) func main() { - ctx := context.Background() - cfg, err := config.LoadAppConfig() if err != nil { slog.Error("error loading app config", "error", err) return } + cliApp := cli.NewApp() + cliApp.Name = cfg.AppName + cliApp.Version = "1.0.0" + cliApp.Commands = []cli.Command{ + { + Name: "start", + Usage: "Start HTTP server", + Action: func(c *cli.Context) error { + return startApp(cfg) + }, + }, + { + Name: "migrate", + Usage: "Database migrations", + Subcommands: []cli.Command{ + { + Name: "up", + Usage: "Apply migrations", + Action: func(c *cli.Context) error { + m, _ := db.InitMainDBMigrations(cfg) + m.MigrationsUp(c.Args().First()) + return nil + }, + }, + { + Name: "down", + Usage: "Rollback migrations", + Action: func(c *cli.Context) error { + m, _ := db.InitMainDBMigrations(cfg) + m.MigrationsDown(c.Args().First()) + return nil + }, + }, + { + Name: "create", + Usage: "Create a new migration file", + Action: func(c *cli.Context) error { + m, _ := db.InitMainDBMigrations(cfg) + return m.CreateMigrationFile(c.Args().First()) + }, + }, + }, + }, + } + + if err := cliApp.Run(os.Args); err != nil { + panic(err) + } +} + +func startApp(cfg config.AppConfig) error { + ctx := context.Background() + + slog.Info("Starting CodeCuriosity Application...") + db, err := config.InitDataStore(cfg) if err != nil { slog.Error("error initializing database", "error", err) - return + return err } defer db.Close() bigqueryInstance, err := config.BigqueryInit(ctx, cfg) if err != nil { slog.Error("error initializing bigquery", "error", err) - return + return err } httpClient := &http.Client{} @@ -47,11 +103,20 @@ func main() { newCronSchedular := cronJob.NewCronSchedular() newCronSchedular.InitCronJobs(dependencies.ContributionService, dependencies.UserService) + c := cors.New(cors.Options{ + AllowedOrigins: []string{"*"}, + AllowCredentials: true, + AllowedMethods: []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete, http.MethodOptions}, + AllowedHeaders: []string{"*"}, + }) + server := http.Server{ Addr: fmt.Sprintf(":%s", cfg.HTTPServer.Port), Handler: router, } + server.Handler = c.Handler(server.Handler) + serverRunning := make(chan os.Signal, 1) signal.Notify( @@ -83,4 +148,5 @@ func main() { } slog.Info("server shutdown successfully") -} + return nil +} \ No newline at end of file diff --git a/backend/go.mod b/backend/go.mod index e73f2e26..6cdfd1ee 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -10,17 +10,24 @@ require ( github.com/jmoiron/sqlx v1.4.0 github.com/lib/pq v1.10.9 github.com/robfig/cron/v3 v3.0.1 + golang.org/x/crypto v0.37.0 golang.org/x/oauth2 v0.29.0 google.golang.org/api v0.231.0 ) +require ( + github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/urfave/cli v1.22.17 // indirect +) + require ( cloud.google.com/go v0.121.0 // indirect cloud.google.com/go/auth v0.16.1 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect cloud.google.com/go/compute/metadata v0.6.0 // indirect cloud.google.com/go/iam v1.5.2 // indirect - github.com/BurntSushi/toml v1.2.1 // indirect + github.com/BurntSushi/toml v1.5.0 // indirect github.com/apache/arrow/go/v15 v15.0.2 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-logr/logr v1.4.2 // indirect @@ -37,6 +44,7 @@ require ( github.com/klauspost/compress v1.16.7 // indirect github.com/klauspost/cpuid/v2 v2.2.5 // indirect github.com/pierrec/lz4/v4 v4.1.18 // indirect + github.com/rs/cors v1.11.1 github.com/zeebo/xxh3 v1.0.2 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect @@ -45,7 +53,6 @@ require ( go.opentelemetry.io/otel/metric v1.35.0 // indirect go.opentelemetry.io/otel/trace v1.35.0 // indirect go.uber.org/atomic v1.11.0 // indirect - golang.org/x/crypto v0.37.0 // indirect golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect golang.org/x/mod v0.23.0 // indirect golang.org/x/net v0.39.0 // indirect diff --git a/backend/go.sum b/backend/go.sum index a5992483..9f239732 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -26,6 +26,8 @@ github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25 github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 h1:ErKg/3iS1AKcTkf3yixlZ54f9U1rljCkQyEXWUnIUxc= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0/go.mod h1:yAZHSGnqScoU556rBOVkwLze6WP5N+U11RHuWaGVxwY= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 h1:fYE9p3esPxA/C0rQ0AHhP0drtPXDRhaWiwg1DPqO7IU= @@ -40,6 +42,9 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42 h1:Om6kYQYDUk5wWbT0t0q6pvyM49i9XZAv9dDrkDA7gjk= github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= +github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +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/dhui/dktest v0.4.5 h1:uUfYBIVREmj/Rw6MvgmqNAYzTiKOHJak+enB5Di73MM= @@ -137,10 +142,23 @@ github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= +github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE= github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/urfave/cli v1.22.17 h1:SYzXoiPfQjHBbkYxbew5prZHS1TOLT3ierW8SYLqtVQ= +github.com/urfave/cli v1.22.17/go.mod h1:b0ht0aqgH/6pBYzzxURyrM4xXNgsoT/n2ZzwQiEhNVo= github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM= @@ -207,6 +225,8 @@ google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +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= olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 h1:slmdOY3vp8a7KQbHkL+FLbvbkgMqmXojpFUO/jENuqQ= diff --git a/backend/internal/config/app.go b/backend/internal/config/app.go index 4070c0f9..2f5f704b 100644 --- a/backend/internal/config/app.go +++ b/backend/internal/config/app.go @@ -30,6 +30,7 @@ type BigqueryProject struct { } type AppConfig struct { + AppName string `yaml:"app_name"` IsProduction bool `yaml:"is_production"` HTTPServer HTTPServer `yaml:"http_server"` Database Database `yaml:"database"` diff --git a/backend/internal/db/migrate.go b/backend/internal/db/migrate.go index 6b9a159f..25081a60 100644 --- a/backend/internal/db/migrate.go +++ b/backend/internal/db/migrate.go @@ -1,4 +1,4 @@ -package main +package db import ( "errors" From 82885db9cd9d78618b4c9c881a1ee240a9c4705c Mon Sep 17 00:00:00 2001 From: VishakhaSainani-Josh Date: Tue, 19 Aug 2025 15:25:35 +0530 Subject: [PATCH 21/36] fixes --- backend/internal/app/auth/domain.go | 4 ++-- backend/internal/app/contribution/service.go | 6 ++++-- backend/internal/app/github/domain.go | 12 ++++++------ frontend/src/api/queries/Auth.ts | 3 ++- .../features/Login/components/LoginComponent.tsx | 15 ++++----------- .../components/Contributors.tsx | 4 ++-- .../components/ContributorsCard.tsx | 0 .../components/Languages.tsx | 2 +- .../components/LanguagesCard.tsx | 0 .../components/Repository.tsx | 0 .../components/RepositoryActivities.tsx | 2 +- .../components/RepositoryCard.tsx | 0 .../index.tsx | 0 frontend/src/root/routes-config.tsx | 2 +- .../src/shared/components/common/CoinsInfo.tsx | 2 +- frontend/src/shared/types/types.ts | 4 ++-- 16 files changed, 26 insertions(+), 30 deletions(-) rename frontend/src/features/{RepositoryDetails.tsx => RepositoryDetails}/components/Contributors.tsx (95%) rename frontend/src/features/{RepositoryDetails.tsx => RepositoryDetails}/components/ContributorsCard.tsx (100%) rename frontend/src/features/{RepositoryDetails.tsx => RepositoryDetails}/components/Languages.tsx (88%) rename frontend/src/features/{RepositoryDetails.tsx => RepositoryDetails}/components/LanguagesCard.tsx (100%) rename frontend/src/features/{RepositoryDetails.tsx => RepositoryDetails}/components/Repository.tsx (100%) rename frontend/src/features/{RepositoryDetails.tsx => RepositoryDetails}/components/RepositoryActivities.tsx (98%) rename frontend/src/features/{RepositoryDetails.tsx => RepositoryDetails}/components/RepositoryCard.tsx (100%) rename frontend/src/features/{RepositoryDetails.tsx => RepositoryDetails}/index.tsx (100%) diff --git a/backend/internal/app/auth/domain.go b/backend/internal/app/auth/domain.go index 81687500..29d28d80 100644 --- a/backend/internal/app/auth/domain.go +++ b/backend/internal/app/auth/domain.go @@ -34,9 +34,9 @@ type User struct { type GithubUserResponse struct { GithubId int `json:"id"` GithubUsername string `json:"login"` - AvatarUrl string `json:"avatarUrl"` + AvatarUrl string `json:"avatar_url"` Email string `json:"email"` - IsAdmin bool `json:"isAdmin"` + IsAdmin bool `json:"is_admin"` } type AdminLoginRequest struct { diff --git a/backend/internal/app/contribution/service.go b/backend/internal/app/contribution/service.go index 6a2d0631..8d2f21a0 100644 --- a/backend/internal/app/contribution/service.go +++ b/backend/internal/app/contribution/service.go @@ -179,8 +179,10 @@ func (s *service) GetContributionType(ctx context.Context, contribution Contribu var stateReason string if issuePayload, ok := contributionPayload[PayloadIssueKey]; ok { issue = issuePayload.(map[string]interface{}) - if stateReasonVal, ok := issue[PayloadStateReasonKey]; ok { - stateReason = stateReasonVal.(string) + if stateReasonVal, ok := issue[PayloadStateReasonKey]; ok && stateReasonVal != nil { + if v, ok := stateReasonVal.(string); ok { + stateReason = v + } } } diff --git a/backend/internal/app/github/domain.go b/backend/internal/app/github/domain.go index 1bc0916b..94ab5438 100644 --- a/backend/internal/app/github/domain.go +++ b/backend/internal/app/github/domain.go @@ -12,11 +12,11 @@ type FetchRepositoryDetailsResponse struct { Id int `json:"id"` Name string `json:"name"` Description string `json:"description"` - LanguagesURL string `json:"languagesUrl"` - UpdateDate time.Time `json:"updatedAt"` + LanguagesURL string `json:"languages_url"` + UpdateDate time.Time `json:"updated_at"` RepoOwnerName RepoOwner `json:"owner"` - ContributorsUrl string `json:"contributorsUrl"` - RepoUrl string `json:"repoUrl"` + ContributorsUrl string `json:"contributors_url"` + RepoUrl string `json:"html_url"` } type RepoLanguages map[string]int @@ -32,7 +32,7 @@ type RepoContributorsResponse struct { type FetchRepositoryContributorsResponse struct { Id int `json:"id"` Name string `json:"name"` - AvatarUrl string `json:"avatarUrl"` - GithubUrl string `json:"githubUrl"` + AvatarUrl string `json:"avatar_url"` + GithubUrl string `json:"github_url"` Contributions int `json:"contributions"` } diff --git a/frontend/src/api/queries/Auth.ts b/frontend/src/api/queries/Auth.ts index c2c7a18e..3cbc3797 100644 --- a/frontend/src/api/queries/Auth.ts +++ b/frontend/src/api/queries/Auth.ts @@ -17,6 +17,7 @@ export const useGithubOauthLogin = (code: string | null) => { return useQuery({ queryKey: [GITHUB_OAUTH_LOGIN_QUERY_KEY, code], queryFn: () => githubOauthLogin(code!), - enabled: !!code + enabled: !!code, + retry: false, }); }; diff --git a/frontend/src/features/Login/components/LoginComponent.tsx b/frontend/src/features/Login/components/LoginComponent.tsx index bd035452..f0d48276 100644 --- a/frontend/src/features/Login/components/LoginComponent.tsx +++ b/frontend/src/features/Login/components/LoginComponent.tsx @@ -12,18 +12,16 @@ import { useEffect } from "react"; import { setAccessToken } from "@/shared/utils/local-storage"; import { useGithubOauthLogin } from "@/api/queries/Auth"; import { toast } from "sonner"; -import { useLoggedInUser } from "@/api/queries/UserProfileDetails"; -import { ACCOUNT_INFO_PATH } from "@/shared/constants/routes"; import githubIcon from "@/assets/github-white-icon.svg"; const LoginComponent = () => { const [searchParams] = useSearchParams(); const navigate = useNavigate(); const code = searchParams.get("code"); + console.log("code", code); const { data, isSuccess, isError } = useGithubOauthLogin(code); - - const { refetch } = useLoggedInUser(false); - + console.log("data", data); + console.log("isSuccess", isSuccess); useEffect(() => { if (!code) return; @@ -31,12 +29,7 @@ const LoginComponent = () => { const token = data.data; setAccessToken(token); - refetch().then(({ data: userData }) => { - if (userData?.data?.isBlocked) { - navigate(ACCOUNT_INFO_PATH); - } - }); - + console.log("hello"); navigate("/"); } diff --git a/frontend/src/features/RepositoryDetails.tsx/components/Contributors.tsx b/frontend/src/features/RepositoryDetails/components/Contributors.tsx similarity index 95% rename from frontend/src/features/RepositoryDetails.tsx/components/Contributors.tsx rename to frontend/src/features/RepositoryDetails/components/Contributors.tsx index 70c01cc4..76b64a2b 100644 --- a/frontend/src/features/RepositoryDetails.tsx/components/Contributors.tsx +++ b/frontend/src/features/RepositoryDetails/components/Contributors.tsx @@ -49,8 +49,8 @@ const ContributorsList = () => { ))} diff --git a/frontend/src/features/RepositoryDetails.tsx/components/ContributorsCard.tsx b/frontend/src/features/RepositoryDetails/components/ContributorsCard.tsx similarity index 100% rename from frontend/src/features/RepositoryDetails.tsx/components/ContributorsCard.tsx rename to frontend/src/features/RepositoryDetails/components/ContributorsCard.tsx diff --git a/frontend/src/features/RepositoryDetails.tsx/components/Languages.tsx b/frontend/src/features/RepositoryDetails/components/Languages.tsx similarity index 88% rename from frontend/src/features/RepositoryDetails.tsx/components/Languages.tsx rename to frontend/src/features/RepositoryDetails/components/Languages.tsx index 3238ed4b..d47572f6 100644 --- a/frontend/src/features/RepositoryDetails.tsx/components/Languages.tsx +++ b/frontend/src/features/RepositoryDetails/components/Languages.tsx @@ -1,7 +1,7 @@ import { type FC } from "react"; -import LanguageCard from "../../RepositoryDetails.tsx/components/LanguagesCard"; import { useRepositoryLanguages } from "@/api/queries/Languages"; import { useParams } from "react-router-dom"; +import LanguageCard from "./LanguagesCard"; interface LanguagesProps { className?: string; diff --git a/frontend/src/features/RepositoryDetails.tsx/components/LanguagesCard.tsx b/frontend/src/features/RepositoryDetails/components/LanguagesCard.tsx similarity index 100% rename from frontend/src/features/RepositoryDetails.tsx/components/LanguagesCard.tsx rename to frontend/src/features/RepositoryDetails/components/LanguagesCard.tsx diff --git a/frontend/src/features/RepositoryDetails.tsx/components/Repository.tsx b/frontend/src/features/RepositoryDetails/components/Repository.tsx similarity index 100% rename from frontend/src/features/RepositoryDetails.tsx/components/Repository.tsx rename to frontend/src/features/RepositoryDetails/components/Repository.tsx diff --git a/frontend/src/features/RepositoryDetails.tsx/components/RepositoryActivities.tsx b/frontend/src/features/RepositoryDetails/components/RepositoryActivities.tsx similarity index 98% rename from frontend/src/features/RepositoryDetails.tsx/components/RepositoryActivities.tsx rename to frontend/src/features/RepositoryDetails/components/RepositoryActivities.tsx index a1287390..ac6e981a 100644 --- a/frontend/src/features/RepositoryDetails.tsx/components/RepositoryActivities.tsx +++ b/frontend/src/features/RepositoryDetails/components/RepositoryActivities.tsx @@ -3,7 +3,7 @@ import clsx from "clsx"; import { Button } from "@/shared/components/ui/button"; import { Card } from "@/shared/components/ui/card"; import ActivityCard from "@/shared/components/common/ActivityCard"; -import { Link, useParams } from "react-router-dom"; +import { useParams } from "react-router-dom"; import { TrendingUp } from "lucide-react"; import { useRepositoryActivities } from "@/api/queries/RepostoryActivities"; import CoinsInfo from "@/shared/components/common/CoinsInfo"; diff --git a/frontend/src/features/RepositoryDetails.tsx/components/RepositoryCard.tsx b/frontend/src/features/RepositoryDetails/components/RepositoryCard.tsx similarity index 100% rename from frontend/src/features/RepositoryDetails.tsx/components/RepositoryCard.tsx rename to frontend/src/features/RepositoryDetails/components/RepositoryCard.tsx diff --git a/frontend/src/features/RepositoryDetails.tsx/index.tsx b/frontend/src/features/RepositoryDetails/index.tsx similarity index 100% rename from frontend/src/features/RepositoryDetails.tsx/index.tsx rename to frontend/src/features/RepositoryDetails/index.tsx diff --git a/frontend/src/root/routes-config.tsx b/frontend/src/root/routes-config.tsx index a6726725..e8247d37 100644 --- a/frontend/src/root/routes-config.tsx +++ b/frontend/src/root/routes-config.tsx @@ -13,11 +13,11 @@ import { REPOSITORY_DETAILS_PATH, USER_DASHBOARD_PATH } from "@/shared/constants/routes"; -import RepositoryDetails from "@/features/RepositoryDetails.tsx"; import AdminLogin from "@/features/Admin/AdminLogin"; import { AllUsersList } from "@/features/Admin/Users.tsx"; import ScoreConfigure from "@/features/Admin/ScoreConfigure"; import BlockedAccountPage from "@/shared/components/common/BlockedAccountPage"; +import RepositoryDetails from "@/features/RepositoryDetails"; export interface RoutesType { path: string; element: ReactNode; diff --git a/frontend/src/shared/components/common/CoinsInfo.tsx b/frontend/src/shared/components/common/CoinsInfo.tsx index 2a621904..5dd3e573 100644 --- a/frontend/src/shared/components/common/CoinsInfo.tsx +++ b/frontend/src/shared/components/common/CoinsInfo.tsx @@ -17,7 +17,7 @@ const CoinsInfo = () => { diff --git a/frontend/src/shared/types/types.ts b/frontend/src/shared/types/types.ts index 75ad6331..ea013146 100644 --- a/frontend/src/shared/types/types.ts +++ b/frontend/src/shared/types/types.ts @@ -97,8 +97,8 @@ export interface Repository { export interface Contributor { id: number; name: string; - avatarUrl: string; - githubUrl: string; + avatar_url: string; + github_url: string; contributions: number; } From bbc2b9dd82213ba5fda40a5091fa9e2464fb9ba7 Mon Sep 17 00:00:00 2001 From: VishakhaSainani-Josh Date: Tue, 2 Sep 2025 18:22:31 +0530 Subject: [PATCH 22/36] refactor bugs --- .gitignore | 4 + backend/internal/app/auth/domain.go | 1 + backend/internal/app/auth/service.go | 4 +- backend/internal/app/contribution/service.go | 23 +- backend/internal/app/cronJob/dailyJob.go | 2 +- backend/internal/app/cronJob/init.go | 8 +- backend/internal/app/cronJob/monthlyJob.go | 32 ++ backend/internal/app/dependencies.go | 2 +- backend/internal/app/goal/domain.go | 100 ++++- backend/internal/app/goal/handler.go | 71 ++- backend/internal/app/goal/service.go | 416 +++++++++++++++--- backend/internal/app/router.go | 34 +- backend/internal/app/user/domain.go | 1 + backend/internal/app/user/handler.go | 62 +-- backend/internal/app/user/service.go | 26 +- ...858_remove-current-active-goal-id.down.sql | 2 + ...07858_remove-current-active-goal-id.up.sql | 1 + ...1756407007_drop-goal-contribution.down.sql | 16 + .../1756407007_drop-goal-contribution.up.sql | 1 + ...6407040_rename-goal-to-goal_level.down.sql | 1 + ...756407040_rename-goal-to-goal_level.up.sql | 1 + ...33_create-table-goal-level-target.down.sql | 1 + ...8433_create-table-goal-level-target.up.sql | 17 + ...1756408501_create-table-user_goal.down.sql | 1 + .../1756408501_create-table-user_goal.up.sql | 18 + ...866_create-table-user_goal_target.down.sql | 1 + ...08866_create-table-user_goal_target.up.sql | 20 + ...8_create-table-user_goal_progress.down.sql | 1 + ...008_create-table-user_goal_progress.up.sql | 16 + ...6810339_create-table-goal-summary.down.sql | 1 + ...756810339_create-table-goal-summary.up.sql | 10 + backend/internal/pkg/apperrors/errors.go | 18 +- backend/internal/pkg/jwt/jwt.go | 4 +- backend/internal/pkg/middleware/middleware.go | 25 +- backend/internal/repository/badge.go | 6 + backend/internal/repository/contribution.go | 22 + backend/internal/repository/domain.go | 59 ++- backend/internal/repository/goal.go | 337 +++++++++++--- backend/internal/repository/user.go | 27 +- frontend/src/api/queries/UserGoals.ts | 93 ++-- frontend/src/features/Admin/AdminLogin.tsx | 59 ++- .../src/features/Admin/ScoreConfigure.tsx | 37 +- .../Login/components/LoginComponent.tsx | 2 +- .../components/Repositories.tsx | 38 +- .../src/features/MyContributions/index.tsx | 13 +- .../components/ContributorsCard.tsx | 2 +- .../components/RepositoryCard.tsx | 27 +- .../src/features/RepositoryDetails/index.tsx | 14 +- .../components/UserDashboard/UserEmail.tsx | 19 +- .../components/UserDashboard/UserGoals.tsx | 200 +++++---- .../UserDashboard/UserProfileDetails.tsx | 21 +- .../UserDashboard/UserProfileMenu.tsx | 16 +- frontend/src/shared/components/ui/button.tsx | 12 +- .../src/shared/constants/local-storage.ts | 1 + frontend/src/shared/constants/query-keys.ts | 3 +- frontend/src/shared/layout/AdminLayout.tsx | 2 +- frontend/src/shared/layout/AuthLayout.tsx | 2 +- frontend/src/shared/types/types.ts | 45 +- frontend/src/shared/utils/local-storage.ts | 22 +- 59 files changed, 1504 insertions(+), 516 deletions(-) create mode 100644 backend/internal/app/cronJob/monthlyJob.go create mode 100644 backend/internal/db/migrations/1755607858_remove-current-active-goal-id.down.sql create mode 100644 backend/internal/db/migrations/1755607858_remove-current-active-goal-id.up.sql create mode 100644 backend/internal/db/migrations/1756407007_drop-goal-contribution.down.sql create mode 100644 backend/internal/db/migrations/1756407007_drop-goal-contribution.up.sql create mode 100644 backend/internal/db/migrations/1756407040_rename-goal-to-goal_level.down.sql create mode 100644 backend/internal/db/migrations/1756407040_rename-goal-to-goal_level.up.sql create mode 100644 backend/internal/db/migrations/1756408433_create-table-goal-level-target.down.sql create mode 100644 backend/internal/db/migrations/1756408433_create-table-goal-level-target.up.sql create mode 100644 backend/internal/db/migrations/1756408501_create-table-user_goal.down.sql create mode 100644 backend/internal/db/migrations/1756408501_create-table-user_goal.up.sql create mode 100644 backend/internal/db/migrations/1756408866_create-table-user_goal_target.down.sql create mode 100644 backend/internal/db/migrations/1756408866_create-table-user_goal_target.up.sql create mode 100644 backend/internal/db/migrations/1756409008_create-table-user_goal_progress.down.sql create mode 100644 backend/internal/db/migrations/1756409008_create-table-user_goal_progress.up.sql create mode 100644 backend/internal/db/migrations/1756810339_create-table-goal-summary.down.sql create mode 100644 backend/internal/db/migrations/1756810339_create-table-goal-summary.up.sql diff --git a/.gitignore b/.gitignore index 0197edab..3df73e6d 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,7 @@ dist-ssr *.njsproj *.sln *.sw? +backend/secrets/*.json +frontend/Dockerfile +frontend/nginx.conf +frontend/schema.sql diff --git a/backend/internal/app/auth/domain.go b/backend/internal/app/auth/domain.go index 29d28d80..c6c7575f 100644 --- a/backend/internal/app/auth/domain.go +++ b/backend/internal/app/auth/domain.go @@ -37,6 +37,7 @@ type GithubUserResponse struct { AvatarUrl string `json:"avatar_url"` Email string `json:"email"` IsAdmin bool `json:"is_admin"` + IsBlocked bool `json:"is_blocked"` } type AdminLoginRequest struct { diff --git a/backend/internal/app/auth/service.go b/backend/internal/app/auth/service.go index 3e237d23..a35a751a 100644 --- a/backend/internal/app/auth/service.go +++ b/backend/internal/app/auth/service.go @@ -78,7 +78,7 @@ func (s *service) GithubOAuthLoginCallback(ctx context.Context, code string) (st } } - jwtToken, err := jwt.GenerateJWT(userData.Id, userInfo.IsAdmin, s.appCfg) + jwtToken, err := jwt.GenerateJWT(userData.Id, userInfo.IsAdmin, userData.IsBlocked, s.appCfg) if err != nil { slog.Error("error generating jwt", "error", err) return "", apperrors.ErrInternalServer @@ -118,7 +118,7 @@ func (s *service) VerifyAdminCredentials(ctx context.Context, adminCredentials A return Admin{}, apperrors.ErrInvalidCredentials } - jwtToken, err := jwt.GenerateJWT(adminInfo.Id, adminInfo.IsAdmin, s.appCfg) + jwtToken, err := jwt.GenerateJWT(adminInfo.Id, adminInfo.IsAdmin, adminInfo.IsBlocked, s.appCfg) if err != nil { slog.Error("failed to generate jwt token", "error", err) return Admin{}, apperrors.ErrInternalServer diff --git a/backend/internal/app/contribution/service.go b/backend/internal/app/contribution/service.go index 8d2f21a0..fa022edd 100644 --- a/backend/internal/app/contribution/service.go +++ b/backend/internal/app/contribution/service.go @@ -7,6 +7,7 @@ import ( "net/http" "github.com/joshsoftware/code-curiosity-2025/internal/app/bigquery" + "github.com/joshsoftware/code-curiosity-2025/internal/app/goal" repoService "github.com/joshsoftware/code-curiosity-2025/internal/app/repository" "github.com/joshsoftware/code-curiosity-2025/internal/app/transaction" "github.com/joshsoftware/code-curiosity-2025/internal/app/user" @@ -59,6 +60,7 @@ type service struct { repositoryService repoService.Service userService user.Service transactionService transaction.Service + goalService goal.Service httpClient *http.Client } @@ -76,13 +78,14 @@ type Service interface { ConfigureContributionTypeScore(ctx context.Context, configureContributionTypeScore []ConfigureContributionTypeScore) ([]ContributionScore, error) } -func NewService(bigqueryService bigquery.Service, contributionRepository repository.ContributionRepository, repositoryService repoService.Service, userService user.Service, transactionService transaction.Service, httpClient *http.Client) Service { +func NewService(bigqueryService bigquery.Service, contributionRepository repository.ContributionRepository, repositoryService repoService.Service, userService user.Service, transactionService transaction.Service, goalService goal.Service, httpClient *http.Client) Service { return &service{ bigqueryService: bigqueryService, contributionRepository: contributionRepository, repositoryService: repositoryService, userService: userService, transactionService: transactionService, + goalService: goalService, httpClient: httpClient, } } @@ -271,6 +274,24 @@ func (s *service) HandleContributionCreation(ctx context.Context, repositoryID i return Contribution{}, err } + err = s.goalService.SyncUserGoalProgressWithContributions(ctx, user.Id) + if err != nil { + slog.Error("error syncing goal progress with contibutions", "error", err) + return obtainedContribution, err + } + + err = s.goalService.AllocateBadge(ctx, user.Id) + if err != nil { + slog.Error("error allocating badge", "error", err) + return obtainedContribution, err + } + + _, err = s.goalService.CreateUserGoalSummary(ctx, user.Id) + if err != nil { + slog.Error("error creating goal summary for user", "error", err) + return obtainedContribution, err + } + return obtainedContribution, nil } diff --git a/backend/internal/app/cronJob/dailyJob.go b/backend/internal/app/cronJob/dailyJob.go index 67f3e24c..7ab7f55f 100644 --- a/backend/internal/app/cronJob/dailyJob.go +++ b/backend/internal/app/cronJob/dailyJob.go @@ -19,7 +19,7 @@ func NewDailyJob(contributionService contribution.Service) *DailyJob { } func (d *DailyJob) Schedule(s *CronSchedular) error { - _, err := s.cron.AddFunc("0 1 * * *", func() { d.Execute(context.Background(), d.run) }) + _, err := s.cron.AddFunc("0 7 * * *", func() { d.Execute(context.Background(), d.run) }) if err != nil { return err } diff --git a/backend/internal/app/cronJob/init.go b/backend/internal/app/cronJob/init.go index 77d9f78b..a08e8de8 100644 --- a/backend/internal/app/cronJob/init.go +++ b/backend/internal/app/cronJob/init.go @@ -14,13 +14,9 @@ type CronSchedular struct { } func NewCronSchedular() *CronSchedular { - location, err := time.LoadLocation("Asia/Kolkata") - if err != nil { - slog.Error("failed to load IST timezone", "error", err) - } - + //CHANGE AND SET TO UTC TIMEZONE return &CronSchedular{ - cron: cron.New(cron.WithLocation(location)), + cron: cron.New(cron.WithLocation(time.UTC)), } } diff --git a/backend/internal/app/cronJob/monthlyJob.go b/backend/internal/app/cronJob/monthlyJob.go new file mode 100644 index 00000000..ed546e2e --- /dev/null +++ b/backend/internal/app/cronJob/monthlyJob.go @@ -0,0 +1,32 @@ +package cronJob + +import ( + "context" + + "github.com/joshsoftware/code-curiosity-2025/internal/app/goal" +) + +type MonthlyJob struct { + CronJob + goalService goal.Service +} + +func NewMonthlyJob(goalService goal.Service) *MonthlyJob { + return &MonthlyJob{ + goalService: goalService, + CronJob: CronJob{Name: "Update User Goal Status"}, + } +} + +func (m *MonthlyJob) Schedule(s *CronSchedular) error { + _, err := s.cron.AddFunc("0 10 2 * *", func() { m.Execute(context.Background(), m.run) }) + if err != nil { + return err + } + + return nil +} + +func (m *MonthlyJob) run(ctx context.Context) { + m.goalService.UpdateUserGoalStatusMonthly(ctx) +} diff --git a/backend/internal/app/dependencies.go b/backend/internal/app/dependencies.go index 91510b77..7b7983b7 100644 --- a/backend/internal/app/dependencies.go +++ b/backend/internal/app/dependencies.go @@ -47,7 +47,7 @@ func InitDependencies(db *sqlx.DB, appCfg config.AppConfig, client config.Bigque authService := auth.NewService(userService, appCfg) bigqueryService := bigquery.NewService(client, userRepository) transactionService := transaction.NewService(transactionRepository, userService) - contributionService := contribution.NewService(bigqueryService, contributionRepository, repositoryService, userService, transactionService, httpClient) + contributionService := contribution.NewService(bigqueryService, contributionRepository, repositoryService, userService, transactionService, goalService, httpClient) authHandler := auth.NewHandler(authService, appCfg) userHandler := user.NewHandler(userService) diff --git a/backend/internal/app/goal/domain.go b/backend/internal/app/goal/domain.go index 9d3e482d..99c6adbe 100644 --- a/backend/internal/app/goal/domain.go +++ b/backend/internal/app/goal/domain.go @@ -2,31 +2,111 @@ package goal import "time" -type Goal struct { +const ( + GoalStatusInProgress = "inProgress" + GoalStatusCompleted = "completed" + GoalStatusIncomplete = "incomplete" +) + +const ( + GoalLevelBeginner = "Beginner" + GoalLevelIntermediate = "Intermediate" + GoalLevelAdvanced = "Advanced" + GoalLevelCustom = "Custom" +) + +type GoalLevel struct { Id int `json:"id"` Level string `json:"level"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` } -type GoalContribution struct { +type GoalLevelName struct { + Level string `json:"level"` +} + +type UserGoal struct { + Id int `json:"id"` + UserId int `json:"userId"` + GoalLevelId int `json:"goalLevelId"` + Status string `json:"status"` + MonthStartedAt time.Time `json:"monthStartedAt"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} + +type UserGoalTarget struct { + Id int `json:"id"` + UserGoalId int `json:"userGoalId"` + ContributionScoreId int `json:"contributionScoreId"` + Target int `json:"target"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} + +type UserGoalProgress struct { + UserGoalTargetId int `json:"userGoalTargetId"` + ContributionId int `json:"contributionId"` +} + +type Contribution struct { Id int `json:"id"` - GoalId int `json:"goalId"` + UserId int `json:"userId"` + RepositoryId int `json:"repositoryId"` ContributionScoreId int `json:"contributionScoreId"` - TargetCount int `json:"targetCount"` - IsCustom bool `json:"isCustom"` - SetByUserId int `json:"setByUserId"` + ContributionType string `json:"contributionType"` + BalanceChange int `json:"balanceChange"` + ContributedAt time.Time `json:"contributedAt"` + GithubEventId string `json:"githubEventId"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` } -type CustomGoalLevelTarget struct { +type CreateUserGoalRequest struct { + Level string `json:"level"` + CustomTargets []CustomTargetRequest `json:"customTargets,omitempty"` +} + +type CustomTargetRequest struct { ContributionType string `json:"contributionType"` Target int `json:"target"` } -type UserGoalLevelProgress struct { +type GetUserCurrentGoalStatusResponse struct { + UserGoalId int `json:"userGoalId"` + Level string `json:"level"` + Status string `json:"status"` + MonthStartedAt time.Time `json:"monthStartedAt"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + GoalTargetProgress []UserGoalTargetProgress `json:"goalTargetProgress"` +} + +type UserGoalTargetProgress struct { ContributionType string `json:"contributionType"` - TargetCount int `json:"targetCount"` - AchievedCount int `json:"achievedCount"` + Target int `json:"target"` + Progress int `json:"progress"` +} + +type UserGoalIdRequest struct { + UserGoalId int `json:"userGoalId"` } + +type GoalSummary struct { + Id int `json:"id"` + UserId int `json:"userId"` + SnapshotDate time.Time `json:"snapshotDate"` + IncompleteGoalsCount int `json:"incompleteGoalsCount"` + TargetSet int `json:"targetSet"` + TargetCompleted int `json:"targetCompleted"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} + +// type MonthlyGoalSummary struct { +// Day time.Time `json:"Day"` +// IncompleteGoalsCount int `json:"IncompleteGoalsCount"` +// TargetSet int `json:"TargetSet"` +// TargetCompleted int `json:"TargetCompleted"` +// } diff --git a/backend/internal/app/goal/handler.go b/backend/internal/app/goal/handler.go index d1637404..9629503c 100644 --- a/backend/internal/app/goal/handler.go +++ b/backend/internal/app/goal/handler.go @@ -16,9 +16,10 @@ type handler struct { type Handler interface { ListGoalLevels(w http.ResponseWriter, r *http.Request) - GetUserActiveGoalLevel(w http.ResponseWriter, r *http.Request) - CreateCustomGoalLevelTarget(w http.ResponseWriter, r *http.Request) - ListUserGoalLevelProgress(w http.ResponseWriter, r *http.Request) + CreateUserGoalInProgress(w http.ResponseWriter, r *http.Request) + ResetUserCurrentGoalStatus(w http.ResponseWriter, r *http.Request) + GetUserCurrentGoalStatus(w http.ResponseWriter, r *http.Request) + FetchUserMonthlyGoalSummary(w http.ResponseWriter, r *http.Request) } func NewHandler(goalService Service) Handler { @@ -32,7 +33,7 @@ func (h *handler) ListGoalLevels(w http.ResponseWriter, r *http.Request) { gaols, err := h.goalService.ListGoalLevels(ctx) if err != nil { - slog.Error("error fetching users conributed repos", "error", err) + slog.Error("error fetching goal levels", "error", err) status, errorMessage := apperrors.MapError(err) response.WriteJson(w, status, errorMessage, nil) return @@ -41,7 +42,7 @@ func (h *handler) ListGoalLevels(w http.ResponseWriter, r *http.Request) { response.WriteJson(w, http.StatusOK, "goal levels fetched successfully", gaols) } -func (h *handler) GetUserActiveGoalLevel(w http.ResponseWriter, r *http.Request) { +func (h *handler) CreateUserGoalInProgress(w http.ResponseWriter, r *http.Request) { ctx := r.Context() userIdCtxVal := ctx.Value(middleware.UserIdKey) @@ -53,18 +54,26 @@ func (h *handler) GetUserActiveGoalLevel(w http.ResponseWriter, r *http.Request) return } - userGoalLevel, err := h.goalService.GetUserActiveGoalLevel(ctx, userId) + var userSelecetdGoal CreateUserGoalRequest + err := json.NewDecoder(r.Body).Decode(&userSelecetdGoal) if err != nil { - slog.Error("error fetching users active goal level", "error", err) + slog.Error(apperrors.ErrFailedMarshal.Error(), "error", err) + response.WriteJson(w, http.StatusBadRequest, apperrors.ErrInvalidRequestBody.Error(), nil) + return + } + + userGoal, err := h.goalService.CreateUserGoalInProgress(ctx, userSelecetdGoal, userId) + if err != nil { + slog.Error("failed to create user goal status", "error", err) status, errorMessage := apperrors.MapError(err) response.WriteJson(w, status, errorMessage, nil) return } - response.WriteJson(w, http.StatusOK, "user active goal level fetched successfully", userGoalLevel) + response.WriteJson(w, http.StatusOK, "Goal created successfully", userGoal) } -func (h *handler) CreateCustomGoalLevelTarget(w http.ResponseWriter, r *http.Request) { +func (h *handler) ResetUserCurrentGoalStatus(w http.ResponseWriter, r *http.Request) { ctx := r.Context() userIdCtxVal := ctx.Value(middleware.UserIdKey) @@ -76,25 +85,41 @@ func (h *handler) CreateCustomGoalLevelTarget(w http.ResponseWriter, r *http.Req return } - var customGoalLevelTarget []CustomGoalLevelTarget - err := json.NewDecoder(r.Body).Decode(&customGoalLevelTarget) + userResetGoalStatus, err := h.goalService.ResetUserCurrentGoalStatus(ctx, userId) if err != nil { - slog.Error(apperrors.ErrFailedMarshal.Error(), "error", err) - response.WriteJson(w, http.StatusBadRequest, apperrors.ErrInvalidRequestBody.Error(), nil) + slog.Error("error resetting user current goal status", "error", err) + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) return } - createdCustomGoalLevelTargets, err := h.goalService.CreateCustomGoalLevelTarget(ctx, userId, customGoalLevelTarget) + response.WriteJson(w, http.StatusOK, "user current goal status reset successfully", userResetGoalStatus) +} + +func (h *handler) GetUserCurrentGoalStatus(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + userIdCtxVal := ctx.Value(middleware.UserIdKey) + userId, ok := userIdCtxVal.(int) + if !ok { + slog.Error("error obtaining user id from context") + status, errorMessage := apperrors.MapError(apperrors.ErrContextValue) + response.WriteJson(w, status, errorMessage, nil) + return + } + + userCurrentGoalStatus, err := h.goalService.GetUserCurrentGoalStatus(ctx, userId) if err != nil { - slog.Error(apperrors.ErrFailedMarshal.Error(), "error", err) - response.WriteJson(w, http.StatusBadRequest, err.Error(), nil) + slog.Error("error getting current goal status for user", "error", err) + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) return } - response.WriteJson(w, http.StatusOK, "custom goal level targets created successfully", createdCustomGoalLevelTargets) + response.WriteJson(w, http.StatusOK, "user current goal status fetched successfully", userCurrentGoalStatus) } -func (h *handler) ListUserGoalLevelProgress(w http.ResponseWriter, r *http.Request) { +func (h *handler) FetchUserMonthlyGoalSummary(w http.ResponseWriter, r *http.Request) { ctx := r.Context() userIdCtxVal := ctx.Value(middleware.UserIdKey) @@ -106,13 +131,13 @@ func (h *handler) ListUserGoalLevelProgress(w http.ResponseWriter, r *http.Reque return } - userGoalLevelProgress, err := h.goalService.ListUserGoalLevelProgress(ctx, userId) + userMonthlyGoalSummary, err := h.goalService.FetchUserGoalSummary(ctx, userId) if err != nil { - slog.Error("error failed to fetch user goal level progress", "error", err) - response.WriteJson(w, http.StatusBadRequest, err.Error(), nil) + slog.Error("error etching user monthly goal summary", "error", err) + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) return } - response.WriteJson(w, http.StatusOK, "user goal level progress fetched successfully", userGoalLevelProgress) - + response.WriteJson(w, http.StatusOK, "user monthly goal summary fetched successfully", userMonthlyGoalSummary) } diff --git a/backend/internal/app/goal/service.go b/backend/internal/app/goal/service.go index 1824c912..ff9e817f 100644 --- a/backend/internal/app/goal/service.go +++ b/backend/internal/app/goal/service.go @@ -2,10 +2,12 @@ package goal import ( "context" + "errors" "log/slog" "time" "github.com/joshsoftware/code-curiosity-2025/internal/app/badge" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" "github.com/joshsoftware/code-curiosity-2025/internal/repository" ) @@ -16,11 +18,17 @@ type service struct { } type Service interface { - ListGoalLevels(ctx context.Context) ([]Goal, error) - GetGoalIdByGoalLevel(ctx context.Context, level string) (int, error) - GetUserActiveGoalLevel(ctx context.Context, userId int) (string, error) - CreateCustomGoalLevelTarget(ctx context.Context, userId int, customGoalLevelTarget []CustomGoalLevelTarget) ([]GoalContribution, error) - ListUserGoalLevelProgress(ctx context.Context, userId int) ([]UserGoalLevelProgress, error) + ListGoalLevels(ctx context.Context) ([]GoalLevel, error) + CreateUserGoalInProgress(ctx context.Context, userSelecetdGoal CreateUserGoalRequest, userId int) (UserGoal, error) + CreateCustomUserGoalTarget(ctx context.Context, userSelectedCustomGoals []CustomTargetRequest, createdUserGoal UserGoal) ([]UserGoalTarget, error) + SyncUserGoalProgress(ctx context.Context, userGoalTargets []UserGoalTarget, monthStartedAt time.Time, userId int) ([]UserGoalProgress, error) + ResetUserCurrentGoalStatus(ctx context.Context, userId int) (UserGoal, error) + GetUserCurrentGoalStatus(ctx context.Context, userId int) (*GetUserCurrentGoalStatusResponse, error) + AllocateBadge(ctx context.Context, userId int) error + UpdateUserGoalStatusMonthly(ctx context.Context) error + SyncUserGoalProgressWithContributions(ctx context.Context, userId int) error + CreateUserGoalSummary(ctx context.Context, userId int) (GoalSummary, error) + FetchUserGoalSummary(ctx context.Context, userId int) ([]GoalSummary, error) } func NewService(goalRepository repository.GoalRepository, contributionRepository repository.ContributionRepository, badgeService badge.Service) Service { @@ -31,130 +39,398 @@ func NewService(goalRepository repository.GoalRepository, contributionRepository } } -func (s *service) ListGoalLevels(ctx context.Context) ([]Goal, error) { +func (s *service) ListGoalLevels(ctx context.Context) ([]GoalLevel, error) { goals, err := s.goalRepository.ListGoalLevels(ctx, nil) if err != nil { slog.Error("error fetching goal levels", "error", err) return nil, err } - serviceGoals := make([]Goal, len(goals)) - + serviceGoals := make([]GoalLevel, len(goals)) for i, g := range goals { - serviceGoals[i] = Goal(g) + serviceGoals[i] = GoalLevel(g) } return serviceGoals, nil } -func (s *service) GetGoalIdByGoalLevel(ctx context.Context, level string) (int, error) { - goalId, err := s.goalRepository.GetGoalIdByGoalLevel(ctx, nil, level) +func (s *service) CreateUserGoalInProgress(ctx context.Context, userSelecetdGoal CreateUserGoalRequest, userId int) (UserGoal, error) { + now := time.Now().UTC() + monthStartedAt := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.UTC) + + userCurrentGoal, err := s.goalRepository.GetUserCurrentGoal(ctx, nil, userId) + if err == nil { + slog.Error("user already has existing goal set for current month") + return UserGoal(userCurrentGoal), apperrors.ErrUserGoalExists + } else if !errors.Is(err, apperrors.ErrUserGoalNotFound) { + slog.Error("error getting user goal for current month") + return UserGoal{}, err + } + goalLevel, err := s.goalRepository.GetGoalLevelByLevel(ctx, nil, userSelecetdGoal.Level) if err != nil { - slog.Error("failed to get goal id by goal level", "error", err) - return 0, err + slog.Error("error fetching goal id by goal level", "error", err) + return UserGoal{}, err } - return goalId, err -} + userGoal := UserGoal{ + UserId: userId, + GoalLevelId: goalLevel.Id, + Status: GoalStatusInProgress, + MonthStartedAt: monthStartedAt, + } -func (s *service) GetUserActiveGoalLevel(ctx context.Context, userId int) (string, error) { - userGoalLevel, err := s.goalRepository.GetUserActiveGoalLevel(ctx, nil, userId) + createdUserGoal, err := s.goalRepository.CreateUserGoalInProgress(ctx, nil, repository.UserGoal(userGoal)) if err != nil { - slog.Error("error fetching user active gaol level", "error", err) - return "", err + slog.Error("failed to create user goal status", "error", err) + return UserGoal{}, err } - return userGoalLevel, nil -} + var createdUserGoalTargets []UserGoalTarget + + //check if custom + if userSelecetdGoal.Level == GoalLevelCustom { + createdUserGoalTargets, err = s.CreateCustomUserGoalTarget(ctx, userSelecetdGoal.CustomTargets, UserGoal(createdUserGoal)) + if err != nil { + slog.Error("error creating custom goal target", "error", err) + return UserGoal{}, err + } + } + + //check if goal level is not custom + if userSelecetdGoal.Level != GoalLevelCustom { + goalLevelTargets, err := s.goalRepository.FetchGoalLevelTargetByGoalLevel(ctx, nil, goalLevel) + if err != nil { + slog.Error("error fetching goal level target", "error", err) + return UserGoal{}, err + } + + for _, g := range goalLevelTargets { + userGoalTarget := UserGoalTarget{ + UserGoalId: createdUserGoal.Id, + ContributionScoreId: g.ContributionScoreId, + Target: g.Target, + } + + createdUserGoalTarget, err := s.goalRepository.CreateUserGoalTarget(ctx, nil, repository.UserGoalTarget(userGoalTarget)) + if err != nil { + slog.Error("error creeating user goal target", "error", err) + return UserGoal{}, err + } -func (s *service) CreateCustomGoalLevelTarget(ctx context.Context, userId int, customGoalLevelTarget []CustomGoalLevelTarget) ([]GoalContribution, error) { - customGoalLevelId, err := s.GetGoalIdByGoalLevel(ctx, "Custom") + createdUserGoalTargets = append(createdUserGoalTargets, UserGoalTarget(createdUserGoalTarget)) + } + } + + _, err = s.SyncUserGoalProgress(ctx, createdUserGoalTargets, monthStartedAt, userId) if err != nil { - slog.Error("error fetching custom goal level id", "error", err) - return nil, err + slog.Error("error syncing user goal progress", "error", err) + return UserGoal{}, err } - var goalContributions []GoalContribution - goalContributionInfo := make([]GoalContribution, len(customGoalLevelTarget)) - for i, c := range customGoalLevelTarget { - goalContributionInfo[i].GoalId = customGoalLevelId + return UserGoal(createdUserGoal), nil +} + +func (s *service) CreateCustomUserGoalTarget(ctx context.Context, userSelectedCustomGoals []CustomTargetRequest, createdUserGoal UserGoal) ([]UserGoalTarget, error) { + createdUserGoalTargets := make([]UserGoalTarget, len(userSelectedCustomGoals)) - contributionScoreDetails, err := s.contributionRepository.GetContributionScoreDetailsByContributionType(ctx, nil, c.ContributionType) + for _, userSelectedCustomGoal := range userSelectedCustomGoals { + contributionScoreDetails, err := s.contributionRepository.GetContributionScoreDetailsByContributionType(ctx, nil, userSelectedCustomGoal.ContributionType) if err != nil { - slog.Error("error fetching contribution score details by type", "error", err) + slog.Error("error getting contirbution score details for given contribution type", "error", err) return nil, err } - goalContributionInfo[i].ContributionScoreId = contributionScoreDetails.Id - goalContributionInfo[i].TargetCount = c.Target - goalContributionInfo[i].SetByUserId = userId + userGoalTarget := UserGoalTarget{ + UserGoalId: createdUserGoal.Id, + ContributionScoreId: contributionScoreDetails.Id, + Target: userSelectedCustomGoal.Target, + } - goalContribution, err := s.goalRepository.CreateCustomGoalLevelTarget(ctx, nil, repository.GoalContribution(goalContributionInfo[i])) + createdUserGoalTarget, err := s.goalRepository.CreateUserGoalTarget(ctx, nil, repository.UserGoalTarget(userGoalTarget)) if err != nil { - slog.Error("error creating custom goal level target", "error", err) + slog.Error("error creeating user goal target", "error", err) return nil, err } - goalContributions = append(goalContributions, GoalContribution(goalContribution)) + createdUserGoalTargets = append(createdUserGoalTargets, UserGoalTarget(createdUserGoalTarget)) + } + + return createdUserGoalTargets, nil +} + +func (s *service) SyncUserGoalProgress(ctx context.Context, userGoalTargets []UserGoalTarget, monthStartedAt time.Time, userId int) ([]UserGoalProgress, error) { + userContributionsForMonth, err := s.contributionRepository.FetchUserContributionsForMonth(ctx, nil, userId, monthStartedAt) + if err != nil { + slog.Error("error fetching user contributions for month", "error", err) + return nil, err + } + + contributionMap := make(map[int][]Contribution) + for _, c := range userContributionsForMonth { + contributionMap[c.ContributionScoreId] = append(contributionMap[c.ContributionScoreId], Contribution(c)) + } + + var createdUserGoalProgresses []UserGoalProgress + + for _, target := range userGoalTargets { + if contributions, ok := contributionMap[target.ContributionScoreId]; ok { + for _, contribution := range contributions { + userGoalProgress := UserGoalProgress{ + UserGoalTargetId: target.Id, + ContributionId: contribution.Id, + } + + created, err := s.goalRepository.CreateUserGoalProgress(ctx, nil, repository.UserGoalProgress(userGoalProgress)) + if err != nil { + slog.Error("error creating user goal progress", "error", err) + return nil, err + } + + createdUserGoalProgresses = append(createdUserGoalProgresses, UserGoalProgress(created)) + } + } } - return goalContributions, nil + return createdUserGoalProgresses, nil } +func (s *service) ResetUserCurrentGoalStatus(ctx context.Context, userId int) (UserGoal, error) { + userCurrentGoal, err := s.goalRepository.GetUserCurrentGoal(ctx, nil, userId) + if err != nil { + slog.Error("error getting user goal for current month") + return UserGoal{}, err + } -func (s *service) ListUserGoalLevelProgress(ctx context.Context, userId int) ([]UserGoalLevelProgress, error) { - goalLevelSetTargets, err := s.goalRepository.ListUserGoalLevelTargets(ctx, nil, userId) + if time.Since(userCurrentGoal.CreatedAt) > 48*time.Hour || userCurrentGoal.Status != GoalStatusInProgress { + slog.Error("cannot reset goal", "error", err) + return UserGoal{}, apperrors.ErrFailedResettingGoal + } + + userGoal := UserGoal{ + Id: userCurrentGoal.Id, + Status: GoalStatusIncomplete, + } + updatedUserGoal, err := s.goalRepository.UpdateUserGoalStatus(ctx, nil, repository.UserGoal(userGoal)) + if err != nil { + slog.Error("error updating goal status for user", "error", err) + return UserGoal{}, err + } + + return UserGoal(updatedUserGoal), nil +} + +func (s *service) GetUserCurrentGoalStatus(ctx context.Context, userId int) (*GetUserCurrentGoalStatusResponse, error) { + userCurrentGoal, err := s.goalRepository.GetUserCurrentGoal(ctx, nil, userId) if err != nil { - slog.Error("error fetching goal level targets", "error", err) + slog.Error("error getting user goal for current month") return nil, err } - year := int(time.Now().Year()) - month := int(time.Now().Month()) - monthlyContributionCount, err := s.contributionRepository.ListMonthlyContributionSummary(ctx, nil, year, month, userId) + goalLevel, err := s.goalRepository.GetGoalLevelById(ctx, nil, userCurrentGoal.GoalLevelId) if err != nil { - slog.Error("error fetching monthly contribution count", "error", err) + slog.Error("error fetching goal leve by goal level id", "error", err) return nil, err } - contributionCountMap := make(map[string]int) - for _, m := range monthlyContributionCount { - contributionCountMap[m.Type] = m.Count + userCurrentGoalTargets, err := s.goalRepository.ListUserGoalTargetsByUserGoalId(ctx, nil, userCurrentGoal.Id) + if err != nil { + slog.Error("error fetching user goal targets by user goal id", "error", err) + return nil, err } - userGoalLevelProgress := make([]UserGoalLevelProgress, len(goalLevelSetTargets)) - var contributionsCompleted int + goalTargetProgresses := make([]UserGoalTargetProgress, 0, len(userCurrentGoalTargets)) + + var totalTargetsCompleted int + totalTargets := len(userCurrentGoalTargets) - for i, g := range goalLevelSetTargets { - contributionType, err := s.contributionRepository.GetContributionTypeByContributionScoreId(ctx, nil, g.ContributionScoreId) + for _, userCurrentGoalTarget := range userCurrentGoalTargets { + + contributionType, err := s.contributionRepository.GetContributionTypeByContributionScoreId(ctx, nil, userCurrentGoalTarget.ContributionScoreId) if err != nil { - slog.Error("error") + slog.Error("error fetching contribution type by contribution score id", "error", err) return nil, err } - userGoalLevelProgress[i].ContributionType = contributionType - userGoalLevelProgress[i].TargetCount = g.TargetCount - userGoalLevelProgress[i].AchievedCount = contributionCountMap[contributionType] + contributionProgressCount, err := s.goalRepository.GetContributionProgressCount(ctx, nil, userCurrentGoalTarget.Id) + if err != nil { + slog.Error("error fetching contribution progress count", "error", err) + return nil, err + } - if userGoalLevelProgress[i].AchievedCount == g.TargetCount { - contributionsCompleted++ + goalTargetProgress := UserGoalTargetProgress{ + ContributionType: contributionType, + Target: userCurrentGoalTarget.Target, + Progress: contributionProgressCount, } - if contributionsCompleted == len(goalLevelSetTargets) { - userGoalLevel, err := s.goalRepository.GetUserActiveGoalLevel(ctx, nil, userId) - if err != nil { - slog.Error("error fetching user active gaol level", "error", err) - return nil, err - } + if goalTargetProgress.Target == goalTargetProgress.Progress { + totalTargetsCompleted++ + } - _, err = s.badgeService.HandleBadgeCreation(ctx, userId, userGoalLevel) - if err != nil { - slog.Error("error handling user badge creation", "error", err) - return nil, err - } + goalTargetProgresses = append(goalTargetProgresses, goalTargetProgress) + } + + userCurrentGoalStatusResponse := GetUserCurrentGoalStatusResponse{ + UserGoalId: userCurrentGoal.Id, + Level: goalLevel.Level, + Status: userCurrentGoal.Status, + MonthStartedAt: userCurrentGoal.MonthStartedAt, + CreatedAt: userCurrentGoal.UpdatedAt, + UpdatedAt: userCurrentGoal.UpdatedAt, + GoalTargetProgress: goalTargetProgresses, + } + + if totalTargets == totalTargetsCompleted { + userGoal := UserGoal{ + Id: userCurrentGoal.Id, + Status: GoalStatusCompleted, } + _, err = s.goalRepository.UpdateUserGoalStatus(ctx, nil, repository.UserGoal(userGoal)) + if err != nil { + slog.Error("error updating user goal status to complete", "error", err) + return nil, err + } + + _, err = s.badgeService.HandleBadgeCreation(ctx, userId, goalLevel.Level) + if err != nil { + slog.Error("error creating badge", "error", err) + return nil, err + } + } + + return &userCurrentGoalStatusResponse, nil +} + +func (s *service) SyncUserGoalProgressWithContributions(ctx context.Context, userId int) error { + userCurrentGoal, err := s.goalRepository.GetUserCurrentGoal(ctx, nil, userId) + if err != nil { + slog.Error("error getting user goal for current month", "error", err) + return err + } + + userCurrentGoalTargets, err := s.goalRepository.ListUserGoalTargetsByUserGoalId(ctx, nil, userCurrentGoal.Id) + if err != nil { + slog.Error("error fetching user goal targets by user goal id", "error", err) + return err + } + + serviceUserCurrentGoalTargets := make([]UserGoalTarget, 0) + for i, userCurrentGoalTarget := range userCurrentGoalTargets { + serviceUserCurrentGoalTargets[i] = UserGoalTarget(userCurrentGoalTarget) + } + + now := time.Now().UTC() + monthStartedAt := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.UTC) + _, err = s.SyncUserGoalProgress(ctx, serviceUserCurrentGoalTargets, monthStartedAt, userId) + if err != nil { + slog.Error("error syncing user goal progress with contributions", "error", err) + return err + } + + return nil +} + +func (s *service) AllocateBadge(ctx context.Context, userId int) error { + userCurrentGoalStatus, err := s.GetUserCurrentGoalStatus(ctx, userId) + if err != nil { + slog.Error("error fetching user current goal status", "error", err) + return err + } + + var totalTargetsCompleted int + totalTargets := len(userCurrentGoalStatus.GoalTargetProgress) + for _, goalTargetProgress := range userCurrentGoalStatus.GoalTargetProgress { + if goalTargetProgress.Progress == goalTargetProgress.Target { + totalTargetsCompleted++ + } + } + + if totalTargets == totalTargetsCompleted { + userGoal := UserGoal{ + Id: userCurrentGoalStatus.UserGoalId, + Status: GoalStatusCompleted, + } + _, err = s.goalRepository.UpdateUserGoalStatus(ctx, nil, repository.UserGoal(userGoal)) + if err != nil { + slog.Error("error updating user goal status to complete", "error", err) + return err + } + + _, err = s.badgeService.HandleBadgeCreation(ctx, userId, userCurrentGoalStatus.Level) + if err != nil { + slog.Error("error creating badge", "error", err) + return err + } + } + + return nil +} + +func (s *service) UpdateUserGoalStatusMonthly(ctx context.Context) error { + userGoals, err := s.goalRepository.FetchInProgressUserGoalsOfPreviousMonth(ctx, nil) + if err != nil { + slog.Error("error getting user goals for previous month", "error", err) + return err + } + + for _, userGoal := range userGoals { + userUpdatedGoal := UserGoal{ + Id: userGoal.Id, + Status: GoalStatusCompleted, + } + _, err = s.goalRepository.UpdateUserGoalStatus(ctx, nil, repository.UserGoal(userUpdatedGoal)) + if err != nil { + slog.Error("error updating users goal for previous month", "error", err) + return err + } + } + + return nil +} + +func (s *service) CreateUserGoalSummary(ctx context.Context, userId int) (GoalSummary, error) { + userIncompleteGoalCount, err := s.goalRepository.CalculateUserIncompleteGoalsUntilDay(ctx, nil, userId) + if err != nil { + slog.Error("error calculating user incomplete goalstatus until day", "error", err) + return GoalSummary{}, err + } + + userCurrentGoalStatus, err := s.GetUserCurrentGoalStatus(ctx, userId) + if err != nil { + slog.Error("error getting user current goal status", "error", err) + return GoalSummary{}, err + } + + var totalTargetSet int + var totalTargetCompleted int + for _, s := range userCurrentGoalStatus.GoalTargetProgress { + totalTargetSet += s.Target + totalTargetCompleted += s.Progress + } + + userMonthlyGoalSummary := GoalSummary{ + UserId: userId, + SnapshotDate: time.Now().UTC(), + IncompleteGoalsCount: userIncompleteGoalCount, + TargetSet: totalTargetSet, + TargetCompleted: totalTargetCompleted, + } + + return userMonthlyGoalSummary, nil +} + +func (s *service) FetchUserGoalSummary(ctx context.Context, userId int) ([]GoalSummary, error) { + usersGoalSummary, err := s.goalRepository.FetchUserGoalSummary(ctx, nil, userId) + if err != nil { + slog.Error("error fetching user goal summary", "error", err) + return nil, err + } + + serviceUserGoalSummary := make([]GoalSummary, 0, len(usersGoalSummary)) + for i, userGoalSummary := range usersGoalSummary { + serviceUserGoalSummary[i] = GoalSummary(userGoalSummary) } - return userGoalLevelProgress, nil + return serviceUserGoalSummary, nil } diff --git a/backend/internal/app/router.go b/backend/internal/app/router.go index e8810972..21ebd575 100644 --- a/backend/internal/app/router.go +++ b/backend/internal/app/router.go @@ -16,29 +16,29 @@ func NewRouter(deps Dependencies) http.Handler { router.HandleFunc("GET /api/v1/auth/github", deps.AuthHandler.GithubOAuthLoginUrl) router.HandleFunc("GET /api/v1/auth/github/callback", deps.AuthHandler.GithubOAuthLoginCallback) - router.HandleFunc("GET /api/v1/auth/user", middleware.Authentication(deps.AuthHandler.GetLoggedInUser, deps.AppCfg)) + router.HandleFunc("GET /api/v1/auth/user", middleware.Authentication(middleware.AuthorizeUnblockedUser(deps.AuthHandler.GetLoggedInUser), deps.AppCfg)) - router.HandleFunc("PATCH /api/v1/user/email", middleware.Authentication(deps.UserHandler.UpdateUserEmail, deps.AppCfg)) - router.HandleFunc("DELETE /api/v1/user/delete/{user_id}", middleware.Authentication(deps.UserHandler.SoftDeleteUser, deps.AppCfg)) + router.HandleFunc("PATCH /api/v1/user/email", middleware.Authentication(middleware.AuthorizeUnblockedUser(deps.UserHandler.UpdateUserEmail), deps.AppCfg)) + router.HandleFunc("DELETE /api/v1/user/delete/{user_id}", middleware.Authentication(middleware.AuthorizeUnblockedUser(deps.UserHandler.SoftDeleteUser), deps.AppCfg)) - router.HandleFunc("GET /api/v1/user/contributions/all", middleware.Authentication(deps.ContributionHandler.FetchUserContributions, deps.AppCfg)) - router.HandleFunc("GET /api/v1/user/overview", middleware.Authentication(deps.ContributionHandler.ListMonthlyContributionSummary, deps.AppCfg)) - router.HandleFunc("GET /api/v1/contributions/types", middleware.Authentication(deps.ContributionHandler.ListAllContributionTypes, deps.AppCfg)) + router.HandleFunc("GET /api/v1/user/contributions/all", middleware.Authentication(middleware.AuthorizeUnblockedUser(deps.ContributionHandler.FetchUserContributions), deps.AppCfg)) + router.HandleFunc("GET /api/v1/user/overview", middleware.Authentication(middleware.AuthorizeUnblockedUser(deps.ContributionHandler.ListMonthlyContributionSummary), deps.AppCfg)) + router.HandleFunc("GET /api/v1/contributions/types", middleware.Authentication(middleware.AuthorizeUnblockedUser(deps.ContributionHandler.ListAllContributionTypes), deps.AppCfg)) - router.HandleFunc("GET /api/v1/user/repositories", middleware.Authentication(deps.RepositoryHandler.FetchUsersContributedRepos, deps.AppCfg)) - router.HandleFunc("GET /api/v1/user/repositories/{repo_id}", middleware.Authentication(deps.RepositoryHandler.FetchParticularRepoDetails, deps.AppCfg)) - router.HandleFunc("GET /api/v1/user/repositories/contributions/recent/{repo_id}", middleware.Authentication(deps.RepositoryHandler.FetchUserContributionsInRepo, deps.AppCfg)) - router.HandleFunc("GET /api/v1/user/repositories/languages/{repo_id}", middleware.Authentication(deps.RepositoryHandler.FetchLanguagePercentInRepo, deps.AppCfg)) - router.HandleFunc("GET /api/v1/user/repositories/contributors/{repo_id}", middleware.Authentication(deps.RepositoryHandler.FetchParticularRepoContributors, deps.AppCfg)) + router.HandleFunc("GET /api/v1/user/repositories", middleware.Authentication(middleware.AuthorizeUnblockedUser(deps.RepositoryHandler.FetchUsersContributedRepos), deps.AppCfg)) + router.HandleFunc("GET /api/v1/user/repositories/{repo_id}", middleware.Authentication(middleware.AuthorizeUnblockedUser(deps.RepositoryHandler.FetchParticularRepoDetails), deps.AppCfg)) + router.HandleFunc("GET /api/v1/user/repositories/contributions/recent/{repo_id}", middleware.Authentication(middleware.AuthorizeUnblockedUser(deps.RepositoryHandler.FetchUserContributionsInRepo), deps.AppCfg)) + router.HandleFunc("GET /api/v1/user/repositories/languages/{repo_id}", middleware.Authentication(middleware.AuthorizeUnblockedUser(deps.RepositoryHandler.FetchLanguagePercentInRepo), deps.AppCfg)) + router.HandleFunc("GET /api/v1/user/repositories/contributors/{repo_id}", middleware.Authentication(middleware.AuthorizeUnblockedUser(deps.RepositoryHandler.FetchParticularRepoContributors), deps.AppCfg)) - router.HandleFunc("GET /api/v1/leaderboard", middleware.Authentication(deps.UserHandler.ListUserRanks, deps.AppCfg)) - router.HandleFunc("GET /api/v1/user/leaderboard", middleware.Authentication(deps.UserHandler.GetCurrentUserRank, deps.AppCfg)) + router.HandleFunc("GET /api/v1/leaderboard", middleware.Authentication(middleware.AuthorizeUnblockedUser(deps.UserHandler.ListUserRanks), deps.AppCfg)) + router.HandleFunc("GET /api/v1/user/leaderboard", middleware.Authentication(middleware.AuthorizeUnblockedUser(deps.UserHandler.GetCurrentUserRank), deps.AppCfg)) router.HandleFunc("GET /api/v1/goal/level", middleware.Authentication(deps.GoalHandler.ListGoalLevels, deps.AppCfg)) - router.HandleFunc("PATCH /api/v1/user/goal/level", middleware.Authentication(deps.UserHandler.UpdateCurrentActiveGoalId, deps.AppCfg)) - router.HandleFunc("GET /api/v1/user/goal/level", middleware.Authentication(deps.GoalHandler.GetUserActiveGoalLevel, deps.AppCfg)) - router.HandleFunc("POST /api/v1/user/goal/level/custom/targets", middleware.Authentication(deps.GoalHandler.CreateCustomGoalLevelTarget, deps.AppCfg)) - router.HandleFunc("GET /api/v1/user/goal/level/progress", middleware.Authentication(deps.GoalHandler.ListUserGoalLevelProgress, deps.AppCfg)) + router.HandleFunc("POST /api/v1/user/goal/level", middleware.Authentication(deps.GoalHandler.CreateUserGoalInProgress, deps.AppCfg)) + router.HandleFunc("POST /api/v1/user/goal/level/reset", middleware.Authentication(deps.GoalHandler.ResetUserCurrentGoalStatus, deps.AppCfg)) + router.HandleFunc("GET /api/v1/user/goal/level", middleware.Authentication(deps.GoalHandler.GetUserCurrentGoalStatus, deps.AppCfg)) + router.HandleFunc("GET /api/v1/user/goal/summary", middleware.Authentication(deps.GoalHandler.FetchUserMonthlyGoalSummary, deps.AppCfg)) router.HandleFunc("GET /api/v1/user/badges", middleware.Authentication(deps.BadgeHandler.GetBadgeDetailsOfUser, deps.AppCfg)) diff --git a/backend/internal/app/user/domain.go b/backend/internal/app/user/domain.go index 8c9a9cc0..5e9a696f 100644 --- a/backend/internal/app/user/domain.go +++ b/backend/internal/app/user/domain.go @@ -28,6 +28,7 @@ type CreateUserRequestBody struct { AvatarUrl string `json:"avatarUrl"` Email string `json:"email"` IsAdmin bool `json:"isAdmin"` + IsBlocked bool `json:"isBlocked"` } type Email struct { diff --git a/backend/internal/app/user/handler.go b/backend/internal/app/user/handler.go index 28f01a44..02f03b37 100644 --- a/backend/internal/app/user/handler.go +++ b/backend/internal/app/user/handler.go @@ -20,7 +20,7 @@ type Handler interface { SoftDeleteUser(w http.ResponseWriter, r *http.Request) ListUserRanks(w http.ResponseWriter, r *http.Request) GetCurrentUserRank(w http.ResponseWriter, r *http.Request) - UpdateCurrentActiveGoalId(w http.ResponseWriter, r *http.Request) + // UpdateCurrentActiveGoalId(w http.ResponseWriter, r *http.Request) ListAllUsers(w http.ResponseWriter, r *http.Request) BlockOrUnblockUser(w http.ResponseWriter, r *http.Request) } @@ -122,36 +122,36 @@ func (h *handler) GetCurrentUserRank(w http.ResponseWriter, r *http.Request) { response.WriteJson(w, http.StatusOK, "current user rank fetched successfully", currentUserRank) } -func (h *handler) UpdateCurrentActiveGoalId(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - - userIdCtxVal := ctx.Value(middleware.UserIdKey) - userId, ok := userIdCtxVal.(int) - if !ok { - slog.Error("error obtaining user id from context") - status, errorMessage := apperrors.MapError(apperrors.ErrContextValue) - response.WriteJson(w, status, errorMessage, nil) - return - } - - var goal GoalLevel - err := json.NewDecoder(r.Body).Decode(&goal) - if err != nil { - slog.Error(apperrors.ErrFailedMarshal.Error(), "error", err) - response.WriteJson(w, http.StatusBadRequest, apperrors.ErrInvalidRequestBody.Error(), nil) - return - } - - goalId, err := h.userService.UpdateCurrentActiveGoalId(ctx, userId, goal.Level) - if err != nil { - slog.Error("failed to update current active goal id", "error", err) - status, errMsg := apperrors.MapError(err) - response.WriteJson(w, status, errMsg, nil) - return - } - - response.WriteJson(w, http.StatusOK, "Goal updated successfully", goalId) -} +// func (h *handler) UpdateCurrentActiveGoalId(w http.ResponseWriter, r *http.Request) { +// ctx := r.Context() + +// userIdCtxVal := ctx.Value(middleware.UserIdKey) +// userId, ok := userIdCtxVal.(int) +// if !ok { +// slog.Error("error obtaining user id from context") +// status, errorMessage := apperrors.MapError(apperrors.ErrContextValue) +// response.WriteJson(w, status, errorMessage, nil) +// return +// } + +// var goal GoalLevel +// err := json.NewDecoder(r.Body).Decode(&goal) +// if err != nil { +// slog.Error(apperrors.ErrFailedMarshal.Error(), "error", err) +// response.WriteJson(w, http.StatusBadRequest, apperrors.ErrInvalidRequestBody.Error(), nil) +// return +// } + +// goalId, err := h.userService.UpdateCurrentActiveGoalId(ctx, userId, goal.Level) +// if err != nil { +// slog.Error("failed to update current active goal id", "error", err) +// status, errMsg := apperrors.MapError(err) +// response.WriteJson(w, status, errMsg, nil) +// return +// } + +// response.WriteJson(w, http.StatusOK, "Goal updated successfully", goalId) +// } func (h *handler) ListAllUsers(w http.ResponseWriter, r *http.Request) { ctx := r.Context() diff --git a/backend/internal/app/user/service.go b/backend/internal/app/user/service.go index 160777eb..05fe19fe 100644 --- a/backend/internal/app/user/service.go +++ b/backend/internal/app/user/service.go @@ -29,7 +29,7 @@ type Service interface { UpdateUserCurrentBalance(ctx context.Context, transaction Transaction) error GetAllUsersRank(ctx context.Context) ([]LeaderboardUser, error) GetCurrentUserRank(ctx context.Context, userId int) (LeaderboardUser, error) - UpdateCurrentActiveGoalId(ctx context.Context, userId int, level string) (int, error) + // UpdateCurrentActiveGoalId(ctx context.Context, userId int, level string) (int, error) GetLoggedInAdmin(ctx context.Context, adminInfo AdminLoginRequest) (User, error) ListAllUsers(ctx context.Context) ([]User, error) BlockOrUnblockUser(ctx context.Context, userID int, block bool) error @@ -186,23 +186,23 @@ func (s *service) GetCurrentUserRank(ctx context.Context, userId int) (Leaderboa return leaderboardUser, nil } -func (s *service) UpdateCurrentActiveGoalId(ctx context.Context, userId int, level string) (int, error) { +// func (s *service) UpdateCurrentActiveGoalId(ctx context.Context, userId int, level string) (int, error) { - goalId, err := s.goalService.GetGoalIdByGoalLevel(ctx, level) +// goalId, err := s.goalService.GetGoalIdByGoalLevel(ctx, level) - if err != nil { - slog.Error("error occured while fetching goal id by goal level") - return 0, err - } +// if err != nil { +// slog.Error("error occured while fetching goal id by goal level") +// return 0, err +// } - goalId, err = s.userRepository.UpdateCurrentActiveGoalId(ctx, nil, userId, goalId) +// goalId, err = s.userRepository.UpdateCurrentActiveGoalId(ctx, nil, userId, goalId) - if err != nil { - slog.Error("failed to update current active goal id", "error", err) - } +// if err != nil { +// slog.Error("failed to update current active goal id", "error", err) +// } - return goalId, err -} +// return goalId, err +// } func (s *service) GetLoggedInAdmin(ctx context.Context, adminInfo AdminLoginRequest) (User, error) { admin, err := s.userRepository.GetAdminByCredentials(ctx, nil, repository.AdminLoginRequest(adminInfo)) diff --git a/backend/internal/db/migrations/1755607858_remove-current-active-goal-id.down.sql b/backend/internal/db/migrations/1755607858_remove-current-active-goal-id.down.sql new file mode 100644 index 00000000..63928214 --- /dev/null +++ b/backend/internal/db/migrations/1755607858_remove-current-active-goal-id.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE users +ADD COLUMN current_active_goal_id BIGINT DEFAULT NULL REFERENCES goal(id); diff --git a/backend/internal/db/migrations/1755607858_remove-current-active-goal-id.up.sql b/backend/internal/db/migrations/1755607858_remove-current-active-goal-id.up.sql new file mode 100644 index 00000000..0d82aa33 --- /dev/null +++ b/backend/internal/db/migrations/1755607858_remove-current-active-goal-id.up.sql @@ -0,0 +1 @@ +ALTER TABLE users DROP COLUMN IF EXISTS current_active_goal_id; \ No newline at end of file diff --git a/backend/internal/db/migrations/1756407007_drop-goal-contribution.down.sql b/backend/internal/db/migrations/1756407007_drop-goal-contribution.down.sql new file mode 100644 index 00000000..472ca2ed --- /dev/null +++ b/backend/internal/db/migrations/1756407007_drop-goal-contribution.down.sql @@ -0,0 +1,16 @@ +CREATE TABLE "goal_contribution"( + "id" SERIAL PRIMARY KEY, + "goal_id" BIGINT NOT NULL, + "contribution_score_id" BIGINT NOT NULL, + "target_count" BIGINT NOT NULL, + "is_custom" BOOLEAN NOT NULL, + "set_by_user_id" BIGINT NOT NULL, + "created_at" TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP +); +ALTER TABLE + "goal_contribution" ADD CONSTRAINT "goal_contribution_set_by_user_id_foreign" FOREIGN KEY("set_by_user_id") REFERENCES "users"("id") ON DELETE CASCADE; +ALTER TABLE + "goal_contribution" ADD CONSTRAINT "goal_contribution_contribution_score_id_foreign" FOREIGN KEY("contribution_score_id") REFERENCES "contribution_score"("id"); +ALTER TABLE + "goal_contribution" ADD CONSTRAINT "goal_contribution_goal_id_foreign" FOREIGN KEY("goal_id") REFERENCES "goal"("id"); \ No newline at end of file diff --git a/backend/internal/db/migrations/1756407007_drop-goal-contribution.up.sql b/backend/internal/db/migrations/1756407007_drop-goal-contribution.up.sql new file mode 100644 index 00000000..be410fff --- /dev/null +++ b/backend/internal/db/migrations/1756407007_drop-goal-contribution.up.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS goal_contribution; \ No newline at end of file diff --git a/backend/internal/db/migrations/1756407040_rename-goal-to-goal_level.down.sql b/backend/internal/db/migrations/1756407040_rename-goal-to-goal_level.down.sql new file mode 100644 index 00000000..501abd0a --- /dev/null +++ b/backend/internal/db/migrations/1756407040_rename-goal-to-goal_level.down.sql @@ -0,0 +1 @@ +ALTER TABLE goal_level RENAME TO goal; \ No newline at end of file diff --git a/backend/internal/db/migrations/1756407040_rename-goal-to-goal_level.up.sql b/backend/internal/db/migrations/1756407040_rename-goal-to-goal_level.up.sql new file mode 100644 index 00000000..43ec540c --- /dev/null +++ b/backend/internal/db/migrations/1756407040_rename-goal-to-goal_level.up.sql @@ -0,0 +1 @@ +ALTER TABLE goal RENAME TO goal_level; \ No newline at end of file diff --git a/backend/internal/db/migrations/1756408433_create-table-goal-level-target.down.sql b/backend/internal/db/migrations/1756408433_create-table-goal-level-target.down.sql new file mode 100644 index 00000000..18723da3 --- /dev/null +++ b/backend/internal/db/migrations/1756408433_create-table-goal-level-target.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS goal_level_target; diff --git a/backend/internal/db/migrations/1756408433_create-table-goal-level-target.up.sql b/backend/internal/db/migrations/1756408433_create-table-goal-level-target.up.sql new file mode 100644 index 00000000..1e2d9c63 --- /dev/null +++ b/backend/internal/db/migrations/1756408433_create-table-goal-level-target.up.sql @@ -0,0 +1,17 @@ +CREATE TABLE "goal_level_target" ( + "id" BIGSERIAL PRIMARY KEY, + "goal_level_id" BIGINT NOT NULL, + "contribution_score_id" BIGINT NOT NULL, + "target" BIGINT NOT NULL, + "created_at" TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP +); + +-- Foreign keys +ALTER TABLE "goal_level_target" + ADD CONSTRAINT "goal_level_target_goal_level_id_fkey" + FOREIGN KEY ("goal_level_id") REFERENCES "goal_level"("id") ON DELETE CASCADE; + +ALTER TABLE "goal_level_target" + ADD CONSTRAINT "goal_level_target_contribution_score_id_fkey" + FOREIGN KEY ("contribution_score_id") REFERENCES "contribution_score"("id"); diff --git a/backend/internal/db/migrations/1756408501_create-table-user_goal.down.sql b/backend/internal/db/migrations/1756408501_create-table-user_goal.down.sql new file mode 100644 index 00000000..06bf86aa --- /dev/null +++ b/backend/internal/db/migrations/1756408501_create-table-user_goal.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS user_goal; diff --git a/backend/internal/db/migrations/1756408501_create-table-user_goal.up.sql b/backend/internal/db/migrations/1756408501_create-table-user_goal.up.sql new file mode 100644 index 00000000..acd7c48c --- /dev/null +++ b/backend/internal/db/migrations/1756408501_create-table-user_goal.up.sql @@ -0,0 +1,18 @@ +CREATE TABLE "user_goal" ( + "id" BIGSERIAL PRIMARY KEY, + "user_id" BIGINT NOT NULL, + "goal_level_id" BIGINT, + "status" VARCHAR NOT NULL, + "month_started_at" TIMESTAMPTZ NOT NULL, + "created_at" TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP +); + +-- Foreign keys +ALTER TABLE "user_goal" + ADD CONSTRAINT "user_goal_user_id_fkey" + FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE; + +ALTER TABLE "user_goal" + ADD CONSTRAINT "user_goal_goal_level_id_fkey" + FOREIGN KEY ("goal_level_id") REFERENCES "goal_level"("id"); diff --git a/backend/internal/db/migrations/1756408866_create-table-user_goal_target.down.sql b/backend/internal/db/migrations/1756408866_create-table-user_goal_target.down.sql new file mode 100644 index 00000000..60e13212 --- /dev/null +++ b/backend/internal/db/migrations/1756408866_create-table-user_goal_target.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS user_goal_target; diff --git a/backend/internal/db/migrations/1756408866_create-table-user_goal_target.up.sql b/backend/internal/db/migrations/1756408866_create-table-user_goal_target.up.sql new file mode 100644 index 00000000..c5ad79b4 --- /dev/null +++ b/backend/internal/db/migrations/1756408866_create-table-user_goal_target.up.sql @@ -0,0 +1,20 @@ +CREATE TABLE "user_goal_target" ( + "id" BIGSERIAL PRIMARY KEY, + "user_goal_id" BIGINT NOT NULL, + "contribution_score_id" BIGINT NOT NULL, + "target" BIGINT NOT NULL, + "created_at" TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "user_goal_target_unique_user_goal_score" + UNIQUE ("user_goal_id", "contribution_score_id") +); + +-- Foreign keys +ALTER TABLE "user_goal_target" + ADD CONSTRAINT "user_goal_target_user_goal_id_fkey" + FOREIGN KEY ("user_goal_id") REFERENCES "user_goal"("id") ON DELETE CASCADE; + +ALTER TABLE "user_goal_target" + ADD CONSTRAINT "user_goal_target_contribution_score_id_fkey" + FOREIGN KEY ("contribution_score_id") REFERENCES "contribution_score"("id"); diff --git a/backend/internal/db/migrations/1756409008_create-table-user_goal_progress.down.sql b/backend/internal/db/migrations/1756409008_create-table-user_goal_progress.down.sql new file mode 100644 index 00000000..fa0fefec --- /dev/null +++ b/backend/internal/db/migrations/1756409008_create-table-user_goal_progress.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS user_goal_progress; diff --git a/backend/internal/db/migrations/1756409008_create-table-user_goal_progress.up.sql b/backend/internal/db/migrations/1756409008_create-table-user_goal_progress.up.sql new file mode 100644 index 00000000..35ca4c99 --- /dev/null +++ b/backend/internal/db/migrations/1756409008_create-table-user_goal_progress.up.sql @@ -0,0 +1,16 @@ +CREATE TABLE "user_goal_progress" ( + "user_goal_target_id" BIGINT NOT NULL, + "contribution_id" BIGINT NOT NULL, + + CONSTRAINT "user_goal_progress_pkey" + PRIMARY KEY ("user_goal_target_id", "contribution_id") +); + +-- Foreign keys +ALTER TABLE "user_goal_progress" + ADD CONSTRAINT "user_goal_progress_user_goal_target_id_fkey" + FOREIGN KEY ("user_goal_target_id") REFERENCES "user_goal_target"("id") ON DELETE CASCADE; + +ALTER TABLE "user_goal_progress" + ADD CONSTRAINT "user_goal_progress_contribution_id_fkey" + FOREIGN KEY ("contribution_id") REFERENCES "contributions"("id") ON DELETE CASCADE; diff --git a/backend/internal/db/migrations/1756810339_create-table-goal-summary.down.sql b/backend/internal/db/migrations/1756810339_create-table-goal-summary.down.sql new file mode 100644 index 00000000..7b8159e1 --- /dev/null +++ b/backend/internal/db/migrations/1756810339_create-table-goal-summary.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS "goal_summary"; \ No newline at end of file diff --git a/backend/internal/db/migrations/1756810339_create-table-goal-summary.up.sql b/backend/internal/db/migrations/1756810339_create-table-goal-summary.up.sql new file mode 100644 index 00000000..6bf6be75 --- /dev/null +++ b/backend/internal/db/migrations/1756810339_create-table-goal-summary.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE "goal_summary" ( + "id" BIGSERIAL PRIMARY KEY, + "user_id" BIGINT NOT NULL REFERENCES "users"("id") ON DELETE CASCADE, + "snapshot_date" TIMESTAMPTZ NOT NULL, + "incomplete_goals_count" BIGINT NOT NULL DEFAULT 0, + "target_set" BIGINT NOT NULL DEFAULT 0, + "target_completed" BIGINT NOT NULL DEFAULT 0, + "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP +); \ No newline at end of file diff --git a/backend/internal/pkg/apperrors/errors.go b/backend/internal/pkg/apperrors/errors.go index ff32c722..ce6fc047 100644 --- a/backend/internal/pkg/apperrors/errors.go +++ b/backend/internal/pkg/apperrors/errors.go @@ -15,6 +15,7 @@ var ( ErrUnauthorizedAccess = errors.New("unauthorized. please provide a valid access token") ErrAccessForbidden = errors.New("access forbidden") + ErrUserBlocked = errors.New("blocked user") ErrInvalidToken = errors.New("invalid or expired token") ErrFailedInitializingLogger = errors.New("failed to initialize logger") @@ -49,13 +50,22 @@ var ( ErrContributionNotFound = errors.New("contribution not found") ErrFetchingContributionTypes = errors.New("failed to fetch all contribution types") ErrNoContributionForContributionType = errors.New("contribution for contribution type does not exist") + ErrFetchingUserContributionsForMonth = errors.New("error fetching user contributions for month") ErrTransactionCreationFailed = errors.New("error failed to create transaction") ErrTransactionNotFound = errors.New("error transaction for the contribution id does not exist") ErrFetchingGoals = errors.New("error fetching goal levels ") - ErrGoalNotFound = errors.New("goal not found") - ErrCustomGoalTargetCreationFailed = errors.New("failed to create targets for custom goal level") + ErrGoalLevelNotFound = errors.New("error goal level does not exist") + ErrFailedToGetGoalLevel = errors.New("error failed to get goal level") + ErrUserGoalCreationFailed = errors.New("error creating user goal") + ErrFetchingGoalLevelTargets = errors.New("error fetching goal level targets") + ErrUserGoalTargetCreationFailed = errors.New("error creating user goal target") + ErrUserGoalProgressCreationFailed = errors.New("error creating user goal progress") + ErrUserGoalNotFound = errors.New("error user does not have any goal set for current month") + ErrFailedToGetUserGoal = errors.New("error failed to get goal set by user in current month") + ErrUserGoalExists = errors.New("error user already has goal set for current month") + ErrFailedResettingGoal = errors.New("error cannot reset goal after 48 hours of creating it or completeing before month") ErrBadgeCreationFailed = errors.New("failed to create badge for user") @@ -70,11 +80,11 @@ func MapError(err error) (statusCode int, errMessage string) { return http.StatusUnauthorized, err.Error() case ErrAccessForbidden: return http.StatusForbidden, err.Error() - case ErrUserNotFound, ErrRepoNotFound, ErrContributionNotFound, ErrGoalNotFound: + case ErrUserNotFound, ErrRepoNotFound, ErrContributionNotFound: return http.StatusNotFound, err.Error() case ErrInvalidToken: return http.StatusUnprocessableEntity, err.Error() default: - return http.StatusInternalServerError, ErrInternalServer.Error() + return http.StatusInternalServerError, err.Error() } } diff --git a/backend/internal/pkg/jwt/jwt.go b/backend/internal/pkg/jwt/jwt.go index 1f5a936b..80a1a84f 100644 --- a/backend/internal/pkg/jwt/jwt.go +++ b/backend/internal/pkg/jwt/jwt.go @@ -9,13 +9,15 @@ import ( type Claims struct { UserId int + IsBlocked bool IsAdmin bool jwt.RegisteredClaims } -func GenerateJWT(userId int, isAdmin bool, appCfg config.AppConfig) (string, error) { +func GenerateJWT(userId int, isAdmin bool, isBlocked bool, appCfg config.AppConfig) (string, error) { claims := Claims{ UserId: userId, + IsBlocked: isBlocked, IsAdmin: isAdmin, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)), diff --git a/backend/internal/pkg/middleware/middleware.go b/backend/internal/pkg/middleware/middleware.go index 3682ee84..9050c363 100644 --- a/backend/internal/pkg/middleware/middleware.go +++ b/backend/internal/pkg/middleware/middleware.go @@ -19,8 +19,9 @@ var txKey = txKeyType{} type contextKey string const ( - UserIdKey contextKey = "userId" - IsAdminKey contextKey = "isAdmin" + UserIdKey contextKey = "userId" + IsBlockedKey contextKey = "isBlocked" + IsAdminKey contextKey = "isAdmin" ) func EmbedTxInContext(ctx context.Context, tx *sqlx.Tx) context.Context { @@ -66,12 +67,32 @@ func Authentication(next http.HandlerFunc, appCfg config.AppConfig) http.Handler ctx := context.WithValue(r.Context(), UserIdKey, userId) isAdmin := token.IsAdmin ctx = context.WithValue(ctx, IsAdminKey, isAdmin) + IsBlocked := token.IsBlocked + ctx = context.WithValue(ctx, IsBlockedKey, IsBlocked) r = r.WithContext(ctx) next.ServeHTTP(w, r) }) } +func AuthorizeUnblockedUser(next http.HandlerFunc) http.HandlerFunc { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + isBlocked, ok := ctx.Value(IsBlockedKey).(bool) + if !ok { + response.WriteJson(w, http.StatusInternalServerError, apperrors.ErrContextValue.Error(), nil) + return + } + + if isBlocked { + response.WriteJson(w, http.StatusForbidden, apperrors.ErrUserBlocked.Error(), nil) + return + } + + next.ServeHTTP(w, r) + }) +} + func AuthorizeAdmin(next http.HandlerFunc) http.HandlerFunc { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() diff --git a/backend/internal/repository/badge.go b/backend/internal/repository/badge.go index 3298c5e6..b1353dbd 100644 --- a/backend/internal/repository/badge.go +++ b/backend/internal/repository/badge.go @@ -2,6 +2,8 @@ package repository import ( "context" + "database/sql" + "errors" "log/slog" "time" @@ -51,6 +53,10 @@ func (br *badgeRepository) GetUserCurrentMonthBadge(ctx context.Context, tx *sql var badge Badge err := executer.GetContext(ctx, &badge, getUserCurrentMonthBadgeQuery, userId) if err != nil { + if errors.Is(err, sql.ErrNoRows) { + slog.Error("badge does not exist for user", "error", err) + return Badge{}, err + } slog.Error("error fetching current month earned badge for user", "error", err) return Badge{}, apperrors.ErrBadgeCreationFailed } diff --git a/backend/internal/repository/contribution.go b/backend/internal/repository/contribution.go index 2d4e17a0..81dfdf8c 100644 --- a/backend/internal/repository/contribution.go +++ b/backend/internal/repository/contribution.go @@ -5,6 +5,7 @@ import ( "database/sql" "errors" "log/slog" + "time" "github.com/jmoiron/sqlx" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" @@ -24,6 +25,7 @@ type ContributionRepository interface { ListMonthlyContributionSummary(ctx context.Context, tx *sqlx.Tx, year int, month int, userId int) ([]MonthlyContributionSummary, error) GetContributionTypeByContributionScoreId(ctx context.Context, tx *sqlx.Tx, contributionScoreId int) (string, error) UpdateContributionTypeScore(ctx context.Context, tx *sqlx.Tx, configureContributionTypeScore []ConfigureContributionTypeScore) ([]ContributionScore, error) + FetchUserContributionsForMonth(ctx context.Context, tx *sqlx.Tx, userId int, monthStartedAt time.Time) ([]Contribution, error) } func NewContributionRepository(db *sqlx.DB) ContributionRepository { @@ -69,6 +71,13 @@ const ( getContributionTypeByContributionScoreIdQuery = `SELECT contribution_type from contribution_score where id=$1` updateContributionTypeScoreQuery = "UPDATE contribution_score SET score = $1 where contribution_type = $2" + + fetchUserContributionsForMonthQuery = ` + SELECT * FROM contributions + WHERE user_id = $1 + AND contributed_at >= $2 + AND contributed_at < ($2 + INTERVAL '1 month'); + ` ) func (cr *contributionRepository) CreateContribution(ctx context.Context, tx *sqlx.Tx, contributionInfo Contribution) (Contribution, error) { @@ -200,3 +209,16 @@ func (cr *contributionRepository) UpdateContributionTypeScore(ctx context.Contex return contributionTypeScores, nil } + +func (cr *contributionRepository) FetchUserContributionsForMonth(ctx context.Context, tx *sqlx.Tx, userId int, monthStartedAt time.Time) ([]Contribution, error) { + executer := cr.BaseRepository.initiateQueryExecuter(tx) + + var userContributionsForMonth []Contribution + err := executer.SelectContext(ctx, &userContributionsForMonth, fetchUserContributionsForMonthQuery, userId, monthStartedAt) + if err != nil { + slog.Error("error fetching user contributions for month", "error", err) + return nil, apperrors.ErrFetchingUserContributionsForMonth + } + + return userContributionsForMonth, nil +} diff --git a/backend/internal/repository/domain.go b/backend/internal/repository/domain.go index b31e667d..3fbaa00f 100644 --- a/backend/internal/repository/domain.go +++ b/backend/internal/repository/domain.go @@ -28,6 +28,7 @@ type CreateUserRequestBody struct { AvatarUrl string `db:"avatar_url"` Email string `db:"email"` IsAdmin bool `db:"is_admin"` + IsBlocked bool `db:"is_blocked"` } type Contribution struct { @@ -93,24 +94,59 @@ type MonthlyContributionSummary struct { Month time.Time `db:"month"` } -type Goal struct { +const ( + GoalStatusInProgress = "inProgress" + GoalStatusCompleted = "completed" + GoalStatusIncomplete = "incomplete" +) + +const ( + GoalLevelBeginner = "Beginner" + GoalLevelIntermediate = "Intermediate" + GoalLevelAdvanced = "Advanced" + GoalLevelCustom = "Custom" +) + +type GoalLevel struct { Id int `db:"id"` Level string `db:"level"` CreatedAt time.Time `db:"created_at"` UpdatedAt time.Time `db:"updated_at"` } -type GoalContribution struct { +type UserGoal struct { + Id int `db:"id"` + UserId int `db:"user_id"` + GoalLevelId int `db:"goal_level_id"` + Status string `db:"status"` + MonthStartedAt time.Time `db:"month_started_at"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` +} + +type GoalLevelTarget struct { + Id int `db:"id"` + GoalLevelId int `db:"goal_level_id"` + ContributionScoreId int `db:"contribution_score_id"` + Target int `db:"target"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` +} + +type UserGoalTarget struct { Id int `db:"id"` - GoalId int `db:"goal_id"` + UserGoalId int `db:"user_goal_id"` ContributionScoreId int `db:"contribution_score_id"` - TargetCount int `db:"target_count"` - IsCustom bool `db:"is_custom"` - SetByUserId int `db:"set_by_user_id"` + Target int `db:"target"` CreatedAt time.Time `db:"created_at"` UpdatedAt time.Time `db:"updated_at"` } +type UserGoalProgress struct { + UserGoalTargetId int `db:"user_goal_target_id"` + ContributionId int `db:"contribution_id"` +} + type Badge struct { Id int `db:"id"` UserId int `db:"user_id"` @@ -129,3 +165,14 @@ type ConfigureContributionTypeScore struct { ContributionType string `db:"contribution_type"` Score int `db:"score"` } + +type GoalSummary struct { + Id int `db:"id"` + UserId int `db:"user_id"` + SnapshotDate time.Time `db:"snapshot_date"` + IncompleteGoalsCount int `db:"incomplete_goals_count"` + TargetSet int `db:"target_set"` + TargetCompleted int `db:"target_completed"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` +} diff --git a/backend/internal/repository/goal.go b/backend/internal/repository/goal.go index ef5a59d0..a129e47b 100644 --- a/backend/internal/repository/goal.go +++ b/backend/internal/repository/goal.go @@ -5,6 +5,7 @@ import ( "database/sql" "errors" "log/slog" + "time" "github.com/jmoiron/sqlx" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" @@ -16,11 +17,20 @@ type goalRepository struct { type GoalRepository interface { RepositoryTransaction - ListGoalLevels(ctx context.Context, tx *sqlx.Tx) ([]Goal, error) - GetGoalIdByGoalLevel(ctx context.Context, tx *sqlx.Tx, level string) (int, error) - ListUserGoalLevelTargets(ctx context.Context, tx *sqlx.Tx, userId int) ([]GoalContribution, error) - CreateCustomGoalLevelTarget(ctx context.Context, tx *sqlx.Tx, customGoalContributionInfo GoalContribution) (GoalContribution, error) - GetUserActiveGoalLevel(ctx context.Context, tx *sqlx.Tx, userId int) (string, error) + ListGoalLevels(ctx context.Context, tx *sqlx.Tx) ([]GoalLevel, error) + GetGoalLevelByLevel(ctx context.Context, tx *sqlx.Tx, level string) (GoalLevel, error) + CreateUserGoalInProgress(ctx context.Context, tx *sqlx.Tx, userGoal UserGoal) (UserGoal, error) + FetchGoalLevelTargetByGoalLevel(ctx context.Context, tx *sqlx.Tx, goalLevel GoalLevel) ([]GoalLevelTarget, error) + CreateUserGoalTarget(ctx context.Context, tx *sqlx.Tx, userGoalTarget UserGoalTarget) (UserGoalTarget, error) + CreateUserGoalProgress(ctx context.Context, tx *sqlx.Tx, userGoalProgress UserGoalProgress) (UserGoalProgress, error) + GetUserCurrentGoal(ctx context.Context, tx *sqlx.Tx, userId int) (UserGoal, error) + UpdateUserGoalStatus(ctx context.Context, tx *sqlx.Tx, userGoal UserGoal) (UserGoal, error) + ListUserGoalTargetsByUserGoalId(ctx context.Context, tx *sqlx.Tx, userGoalId int) ([]UserGoalTarget, error) + GetContributionProgressCount(ctx context.Context, tx *sqlx.Tx, userGoalTargetId int) (int, error) + GetGoalLevelById(ctx context.Context, tx *sqlx.Tx, goalLevelId int) (GoalLevel, error) + FetchInProgressUserGoalsOfPreviousMonth(ctx context.Context, tx *sqlx.Tx) ([]UserGoal, error) + CalculateUserIncompleteGoalsUntilDay(ctx context.Context, tx *sqlx.Tx, userID int) (int, error) + FetchUserGoalSummary(ctx context.Context, tx *sqlx.Tx, userId int) ([]GoalSummary, error) } func NewGoalRepository(db *sqlx.DB) GoalRepository { @@ -30,115 +40,310 @@ func NewGoalRepository(db *sqlx.DB) GoalRepository { } const ( - listGoalLevelQuery = "SELECT * from goal;" - - getGoalIdByGoalLevelQuery = "SELECT id from goal where level=$1" - - listUserGoalLevelTargetsQuery = ` - SELECT * from goal_contribution - where goal_id - IN - (SELECT current_active_goal_id from users where id=$1)` - - createCustomGoalLevelTargetQuery = ` - INSERT INTO goal_contribution( - goal_id, - contribution_score_id, - target_count, - is_custom, - set_by_user_id + listGoalLevelQuery = "SELECT * from goal_level;" + + getGoalLevelByLevelQuery = "SELECT * from goal_level where level=$1" + + createUserGoalInProgressQuery = ` + INSERT INTO user_goal( + user_id, + goal_level_id, + status, + month_started_at + ) + VALUES + ($1, $2, $3, $4) + RETURNING *` + + fetchGoalLevelTargetByGoalLevelQuery = "SELECT * FROM goal_level_target WHERE goal_level_id=$1" + + createUserGoalTargetQuery = ` + INSERT INTO user_goal_target( + user_goal_id, + contribution_score_id, + target + ) + VALUES + ($1, $2, $3) + RETURNING *` + + createUserGoalProgressQuery = ` + INSERT INTO user_goal_progress( + user_goal_target_id, + contribution_id + ) + VALUES + ($1, $2) + RETURNING *` + + getUserCurrentGoalQuery = ` + SELECT * FROM user_goal + WHERE date_trunc('month', month_started_at) = date_trunc('month', NOW()) + AND status != 'incomplete' + AND user_id = $1 + ORDER BY created_at DESC + LIMIT 1` + + updateUserGoalStatusQuery = "UPDATE user_goal SET status=$1, updated_at=$2 WHERE id=$3 RETURNING *" + + listUserGoalTargetsByUserGoalIdQuery = "SELECT * from user_goal_target WHERE user_goal_id=$1" + + getContributionProgressCountQuery = "SELECT COUNT(*) FROM user_goal_progress WHERE user_goal_target_id=$1" + + getGoalLevelByIdQuery = "SELECT * FROM goal_level where id=$1" + + fetchInProgressUserGoalsOfPreviousMonthQuery = ` + SELECT * FROM user_goal + WHERE date_trunc('month', month_started_at) = date_trunc('month', NOW() - interval '1 month') + WHERE status='inProgress'` + + calculateUserIncompleteGoalsUntilDayQuery = ` + SELECT COUNT(*) FROM user_goal + WHERE date_trunc('month', month_started_at) = date_trunc('month', NOW()) + AND status = 'incomplete' + AND user_id = $1` + + createUserGoalSummaryQuery = ` + INSERT INTO goal_summary( + user_id, + snapshot_date, + incomplete_goals_count, + target_set, + target_completed ) - VALUES + VALUES ($1, $2, $3, $4, $5) RETURNING *` - getUserActiveGoalLevelQuery = ` - SELECT level from goal - where id IN - (SELECT current_active_goal_id from users where id=$1)` + fetchUserGoalSummaryQuery = "SELECT * FROM goal_summary WHERE user_id=$1" ) -func (gr *goalRepository) ListGoalLevels(ctx context.Context, tx *sqlx.Tx) ([]Goal, error) { +func (gr *goalRepository) ListGoalLevels(ctx context.Context, tx *sqlx.Tx) ([]GoalLevel, error) { executer := gr.BaseRepository.initiateQueryExecuter(tx) - var goals []Goal - err := executer.SelectContext(ctx, &goals, listGoalLevelQuery) + var goalLevels []GoalLevel + err := executer.SelectContext(ctx, &goalLevels, listGoalLevelQuery) if err != nil { slog.Error("error fetching goal levels", "error", err) return nil, apperrors.ErrFetchingGoals } - return goals, nil + return goalLevels, nil +} + +func (gr *goalRepository) GetGoalLevelByLevel(ctx context.Context, tx *sqlx.Tx, level string) (GoalLevel, error) { + executer := gr.BaseRepository.initiateQueryExecuter(tx) + + var goalLevel GoalLevel + err := executer.GetContext(ctx, &goalLevel, getGoalLevelByLevelQuery, level) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + slog.Error("error goal level not found", "error", err) + return GoalLevel{}, apperrors.ErrGoalLevelNotFound + } + + slog.Error("error occured while getting goal level by level", "error", err) + return GoalLevel{}, apperrors.ErrFailedToGetGoalLevel + } + + return goalLevel, nil +} + +func (gr *goalRepository) CreateUserGoalInProgress(ctx context.Context, tx *sqlx.Tx, userGoal UserGoal) (UserGoal, error) { + executer := gr.BaseRepository.initiateQueryExecuter(tx) + + var createdUserGoal UserGoal + err := executer.GetContext(ctx, &createdUserGoal, createUserGoalInProgressQuery, userGoal.UserId, userGoal.GoalLevelId, userGoal.Status, userGoal.MonthStartedAt) + if err != nil { + slog.Error("failed to create user goal in progress", "error", err) + return UserGoal{}, apperrors.ErrUserGoalCreationFailed + } + + return createdUserGoal, nil +} + +func (gr *goalRepository) FetchGoalLevelTargetByGoalLevel(ctx context.Context, tx *sqlx.Tx, goalLevel GoalLevel) ([]GoalLevelTarget, error) { + executer := gr.BaseRepository.initiateQueryExecuter(tx) + + var goalLevelTargets []GoalLevelTarget + err := executer.SelectContext(ctx, &goalLevelTargets, fetchGoalLevelTargetByGoalLevelQuery, goalLevel.Id) + if err != nil { + slog.Error("error fetching goal level target by goal level", "error", err) + return nil, apperrors.ErrFetchingGoalLevelTargets + } + + return goalLevelTargets, nil +} + +func (gr *goalRepository) CreateUserGoalTarget(ctx context.Context, tx *sqlx.Tx, userGoalTarget UserGoalTarget) (UserGoalTarget, error) { + executer := gr.BaseRepository.initiateQueryExecuter(tx) + + var createdUserGoalTarget UserGoalTarget + err := executer.GetContext(ctx, &createdUserGoalTarget, createUserGoalTargetQuery, userGoalTarget.UserGoalId, userGoalTarget.ContributionScoreId, userGoalTarget.Target) + if err != nil { + slog.Error("error creating user goal target", "error", err) + return UserGoalTarget{}, apperrors.ErrUserGoalTargetCreationFailed + } + + return createdUserGoalTarget, nil } -func (gr *goalRepository) GetGoalIdByGoalLevel(ctx context.Context, tx *sqlx.Tx, level string) (int, error) { +func (gr *goalRepository) CreateUserGoalProgress(ctx context.Context, tx *sqlx.Tx, userGoalProgress UserGoalProgress) (UserGoalProgress, error) { executer := gr.BaseRepository.initiateQueryExecuter(tx) - var goalId int - err := executer.GetContext(ctx, &goalId, getGoalIdByGoalLevelQuery, level) + var createdUserGoalProgress UserGoalProgress + err := executer.GetContext(ctx, &createdUserGoalProgress, createUserGoalProgressQuery, userGoalProgress.UserGoalTargetId, userGoalProgress.ContributionId) + if err != nil { + slog.Error("error creating user goal target", "error", err) + return UserGoalProgress{}, apperrors.ErrUserGoalProgressCreationFailed + } + + return createdUserGoalProgress, nil +} + +func (gr *goalRepository) GetUserCurrentGoal(ctx context.Context, tx *sqlx.Tx, userId int) (UserGoal, error) { + executer := gr.BaseRepository.initiateQueryExecuter(tx) + + var userGoal UserGoal + err := executer.GetContext(ctx, &userGoal, getUserCurrentGoalQuery, userId) if err != nil { if errors.Is(err, sql.ErrNoRows) { - slog.Error("error goal not found", "error", err) - return 0, apperrors.ErrGoalNotFound + slog.Warn("no user goal found for current month", "userId", userId) + return UserGoal{}, apperrors.ErrUserGoalNotFound } - slog.Error("error occured while getting goal id by goal level", "error", err) + slog.Error("error occurred while getting latest user goal for current month", "error", err) + return UserGoal{}, apperrors.ErrFailedToGetUserGoal + } + + return userGoal, nil +} + +func (gr *goalRepository) UpdateUserGoalStatus(ctx context.Context, tx *sqlx.Tx, userGoal UserGoal) (UserGoal, error) { + executer := gr.BaseRepository.initiateQueryExecuter(tx) + + var updatedGoal UserGoal + err := executer.GetContext(ctx, &updatedGoal, updateUserGoalStatusQuery, + userGoal.Status, + time.Now().UTC(), + userGoal.Id, + ) + if err != nil { + slog.Error("failed to update user goal id", "error", err) + return UserGoal{}, apperrors.ErrInternalServer + } + + return updatedGoal, nil +} + +func (gr *goalRepository) ListUserGoalTargetsByUserGoalId(ctx context.Context, tx *sqlx.Tx, userGoalId int) ([]UserGoalTarget, error) { + executer := gr.BaseRepository.initiateQueryExecuter(tx) + + var userGoalTargets []UserGoalTarget + err := executer.SelectContext(ctx, &userGoalTargets, listUserGoalTargetsByUserGoalIdQuery, userGoalId) + if err != nil { + slog.Error("error fetching user goal targets by user goal id", "error", err) + return nil, apperrors.ErrFetchingGoalLevelTargets + } + + return userGoalTargets, nil +} + +func (gr *goalRepository) GetContributionProgressCount(ctx context.Context, tx *sqlx.Tx, userGoalTargetId int) (int, error) { + executer := gr.BaseRepository.initiateQueryExecuter(tx) + + var contributionProgressCount int + err := executer.GetContext(ctx, &contributionProgressCount, getContributionProgressCountQuery, userGoalTargetId) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + slog.Error("no contribution for the target") + return 0, nil + } + + slog.Error("error counting progress of given contribution target", "error", err) return 0, apperrors.ErrInternalServer } - return goalId, nil + return contributionProgressCount, nil } -func (gr *goalRepository) ListUserGoalLevelTargets(ctx context.Context, tx *sqlx.Tx, userId int) ([]GoalContribution, error) { +func (gr *goalRepository) GetGoalLevelById(ctx context.Context, tx *sqlx.Tx, goalLevelId int) (GoalLevel, error) { executer := gr.BaseRepository.initiateQueryExecuter(tx) - var goalLevelTargets []GoalContribution - err := executer.SelectContext(ctx, &goalLevelTargets, listUserGoalLevelTargetsQuery, userId) + var goalLevel GoalLevel + err := executer.GetContext(ctx, &goalLevel, getGoalLevelByIdQuery, goalLevelId) if err != nil { if errors.Is(err, sql.ErrNoRows) { - slog.Error("error goal not found", "error", err) - return nil, apperrors.ErrInternalServer + slog.Error("error goal level not found", "error", err) + return GoalLevel{}, apperrors.ErrGoalLevelNotFound } - slog.Error("error occured while getting goal id by goal level", "error", err) - return nil, apperrors.ErrInternalServer + slog.Error("error occured while getting goal level by id", "error", err) + return GoalLevel{}, apperrors.ErrFailedToGetGoalLevel } - return goalLevelTargets, nil + return goalLevel, nil +} + +func (gr *goalRepository) FetchInProgressUserGoalsOfPreviousMonth(ctx context.Context, tx *sqlx.Tx) ([]UserGoal, error) { + executer := gr.BaseRepository.initiateQueryExecuter(tx) + + var userGoals []UserGoal + err := executer.SelectContext(ctx, &userGoals, fetchInProgressUserGoalsOfPreviousMonthQuery) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + slog.Info("no in progress goals") + return nil, nil + } + slog.Error("error occurred while fetching in progress user goals for previous month", "error", err) + return nil, apperrors.ErrFailedToGetUserGoal + } + + return userGoals, nil +} + +func (gr *goalRepository) CalculateUserIncompleteGoalsUntilDay(ctx context.Context, tx *sqlx.Tx, userID int) (int, error) { + executer := gr.BaseRepository.initiateQueryExecuter(tx) + + var userIncompleteGoalCount int + err := executer.GetContext(ctx, &userIncompleteGoalCount, calculateUserIncompleteGoalsUntilDayQuery, userID) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + slog.Info("no incomplete goals for month") + return 0, nil + } + slog.Error("error getting incomplete goals until day", "error", err) + return 0, err + } + + return userIncompleteGoalCount, nil } -func (gr *goalRepository) CreateCustomGoalLevelTarget(ctx context.Context, tx *sqlx.Tx, customGoalContributionInfo GoalContribution) (GoalContribution, error) { +func (gr *goalRepository) CreateUserGoalSummary(ctx context.Context, tx *sqlx.Tx, userGoalSummary GoalSummary) (GoalSummary, error) { executer := gr.BaseRepository.initiateQueryExecuter(tx) - var customGoalContribution GoalContribution - err := executer.GetContext(ctx, &customGoalContribution, createCustomGoalLevelTargetQuery, - customGoalContributionInfo.GoalId, - customGoalContributionInfo.ContributionScoreId, - customGoalContributionInfo.TargetCount, - true, - customGoalContributionInfo.SetByUserId) + var createdUserGoalSummary GoalSummary + err := executer.GetContext(ctx, &createdUserGoalSummary, createUserGoalSummaryQuery, userGoalSummary.UserId, userGoalSummary.SnapshotDate, userGoalSummary.IncompleteGoalsCount, userGoalSummary.TargetSet, userGoalSummary.TargetCompleted) if err != nil { - slog.Error("error creating custom goal level targets", "error", err) - return GoalContribution{}, apperrors.ErrCustomGoalTargetCreationFailed + slog.Error("error creating user goal summary", "error", err) + return GoalSummary{}, err } - return customGoalContribution, nil + return createdUserGoalSummary, nil } -func (gr *goalRepository) GetUserActiveGoalLevel(ctx context.Context, tx *sqlx.Tx, userId int) (string, error) { +func (gr *goalRepository) FetchUserGoalSummary(ctx context.Context, tx *sqlx.Tx, userId int) ([]GoalSummary, error) { executer := gr.BaseRepository.initiateQueryExecuter(tx) - var userActiveGoalLevel string - err := executer.GetContext(ctx, &userActiveGoalLevel, getUserActiveGoalLevelQuery, userId) + var usersGoalSummary []GoalSummary + err := executer.SelectContext(ctx, &usersGoalSummary, fetchUserGoalSummaryQuery, userId) if err != nil { if errors.Is(err, sql.ErrNoRows) { - slog.Info("user does not have any active goal level") - return "", nil + return nil, nil } - - slog.Error("error getting users current active goal level name", "error", err) - return userActiveGoalLevel, apperrors.ErrInternalServer + slog.Error("error fetching users goal summary", "error", err) + return nil, err } - return userActiveGoalLevel, nil + return usersGoalSummary, nil } diff --git a/backend/internal/repository/user.go b/backend/internal/repository/user.go index 7333694e..1df72b57 100644 --- a/backend/internal/repository/user.go +++ b/backend/internal/repository/user.go @@ -28,7 +28,6 @@ type UserRepository interface { UpdateUserCurrentBalance(ctx context.Context, tx *sqlx.Tx, user User) error GetAllUsersRank(ctx context.Context, tx *sqlx.Tx) ([]LeaderboardUser, error) GetCurrentUserRank(ctx context.Context, tx *sqlx.Tx, userId int) (LeaderboardUser, error) - UpdateCurrentActiveGoalId(ctx context.Context, tx *sqlx.Tx, userId int, goalId int) (int, error) GetAdminByCredentials(ctx context.Context, tx *sqlx.Tx, adminInfo AdminLoginRequest) (User, error) GetAllUsers(ctx context.Context, tx *sqlx.Tx) ([]User, error) UpdateUserBlockStatus(ctx context.Context, tx *sqlx.Tx, userID int, block bool) error @@ -57,9 +56,9 @@ const ( updateEmailQuery = "UPDATE users SET email=$1, updated_at=$2 where id=$3" - markUserAsDeletedQuery = "UPDATE users SET is_deleted = TRUE, deleted_at=$1 where id = $2" + markUserAsDeletedQuery = "UPDATE users SET is_deleted = TRUE, deleted_at=$1, updated_at=$2 where id = $3" - recoverAccountInGracePeriodQuery = "UPDATE users SET is_deleted = false, deleted_at = NULL where id = $1" + recoverAccountInGracePeriodQuery = "UPDATE users SET is_deleted = false, deleted_at = NULL, updated_at=$1 where id = $2" hardDeleteUsersQuery = "DELETE FROM users WHERE is_deleted = TRUE AND deleted_at <= $1" @@ -94,13 +93,11 @@ const ( ranked_users WHERE id = $1;` - updateCurrentActiveGoalIdQuery = "UPDATE users SET current_active_goal_id=$1 where id=$2" - verifyAdminCredentialsQuery = "SELECT * FROM users where email = $1 and is_admin=true" getAllUsersQuery = "SELECT * FROM users where is_admin=false" - updateUserBlockStatusQuery = "UPDATE users SET is_blocked=$1 where id=$2" + updateUserBlockStatusQuery = "UPDATE users SET is_blocked=$1, updated_at=$2 where id=$3" ) func (ur *userRepository) GetUserById(ctx context.Context, tx *sqlx.Tx, userId int) (User, error) { @@ -171,7 +168,7 @@ func (ur *userRepository) UpdateUserEmail(ctx context.Context, tx *sqlx.Tx, user func (ur *userRepository) MarkUserAsDeleted(ctx context.Context, tx *sqlx.Tx, userId int, deletedAt time.Time) error { executer := ur.BaseRepository.initiateQueryExecuter(tx) - _, err := executer.ExecContext(ctx, markUserAsDeletedQuery, deletedAt, userId) + _, err := executer.ExecContext(ctx, markUserAsDeletedQuery, deletedAt, time.Now(), userId) if err != nil { slog.Error("unable to mark user as deleted", "error", err) return apperrors.ErrInternalServer @@ -183,7 +180,7 @@ func (ur *userRepository) MarkUserAsDeleted(ctx context.Context, tx *sqlx.Tx, us func (ur *userRepository) RecoverAccountInGracePeriod(ctx context.Context, tx *sqlx.Tx, userId int) error { executer := ur.BaseRepository.initiateQueryExecuter(tx) - _, err := executer.ExecContext(ctx, recoverAccountInGracePeriodQuery, userId) + _, err := executer.ExecContext(ctx, recoverAccountInGracePeriodQuery, time.Now(), userId) if err != nil { slog.Error("unable to reverse the soft delete ", "error", err) return apperrors.ErrInternalServer @@ -258,18 +255,6 @@ func (ur *userRepository) GetCurrentUserRank(ctx context.Context, tx *sqlx.Tx, u return currentUserRank, nil } -func (ur *userRepository) UpdateCurrentActiveGoalId(ctx context.Context, tx *sqlx.Tx, userId int, goalId int) (int, error) { - executer := ur.BaseRepository.initiateQueryExecuter(tx) - - _, err := executer.ExecContext(ctx, updateCurrentActiveGoalIdQuery, goalId, userId) - if err != nil { - slog.Error("failed to update current active goal id", "error", err) - return 0, apperrors.ErrInternalServer - } - - return goalId, nil -} - func (ur *userRepository) GetAdminByCredentials(ctx context.Context, tx *sqlx.Tx, adminInfo AdminLoginRequest) (User, error) { executer := ur.BaseRepository.initiateQueryExecuter(tx) @@ -303,7 +288,7 @@ func (ur *userRepository) GetAllUsers(ctx context.Context, tx *sqlx.Tx) ([]User, func (ur *userRepository) UpdateUserBlockStatus(ctx context.Context, tx *sqlx.Tx, userID int, block bool) error { executer := ur.BaseRepository.initiateQueryExecuter(tx) - _, err := executer.ExecContext(ctx, updateUserBlockStatusQuery, block, userID) + _, err := executer.ExecContext(ctx, updateUserBlockStatusQuery, block, time.Now(), userID) if err != nil { slog.Error("failed to update user block status", "error", err) return apperrors.ErrInternalServer diff --git a/frontend/src/api/queries/UserGoals.ts b/frontend/src/api/queries/UserGoals.ts index 24b400d4..26453a5a 100644 --- a/frontend/src/api/queries/UserGoals.ts +++ b/frontend/src/api/queries/UserGoals.ts @@ -5,52 +5,17 @@ import { useMutation, useQuery } from "@tanstack/react-query"; import { CONTRIBUTION_TYPES_QUERY_KEY, GOAL_LEVELS_QUERY_KEY, - USER_ACTIVE_GOAL_LEVEL_QUERY_KEY, - USER_GOAL_LEVEL_PROGRESS_QUERY_KEY + USER_ACTIVE_GOAL_LEVEL_QUERY_KEY } from "@/shared/constants/query-keys"; import type { ContributionTypeDetail, - CustomGoalLevelTarget, - CustomGoalLevelTargetResponse, GoalLevel, - GoalLevelProgress + SetUserGoalLevelRequest, + UserCurrentGoalStatus, + UserGoal, + UserGoalLevelStatus } from "@/shared/types/types"; -const fetchUserActiveGoalLevel = async (): Promise> => { - const response = await api.get<{ - message: string; - data: string; - }>(`${BACKEND_URL}/api/v1/user/goal/level`); - - return response.data; -}; - -export const useUserActiveGoalLevel = () => { - return useQuery({ - queryKey: [USER_ACTIVE_GOAL_LEVEL_QUERY_KEY], - queryFn: fetchUserActiveGoalLevel - }); -}; - -const setUserGoalLevel = async ( - selectedLevel: string -): Promise> => { - const response = await api.patch<{ - message: string; - data: number; - }>(`${BACKEND_URL}/api/v1/user/goal/level`, { - level: selectedLevel - }); - - return response.data; -}; - -export const useSetUserGoalLevel = () => { - return useMutation({ - mutationFn: (selectedLevel: string) => setUserGoalLevel(selectedLevel) - }); -}; - const fetchGoalLevels = async (): Promise> => { const response = await api.get<{ message: string; @@ -67,45 +32,57 @@ export const useGoalLevels = () => { }); }; -const fetchUserGoalLevelProgress = async (): Promise< - ApiResponse +const fetchUserCurrentGoalStatus = async (): Promise< + ApiResponse > => { const response = await api.get<{ message: string; - data: GoalLevelProgress[]; - }>(`${BACKEND_URL}/api/v1/user/goal/level/progress`); + data: UserCurrentGoalStatus; + }>(`${BACKEND_URL}/api/v1/user/goal/level`); return response.data; }; -export const useUserGoalLevelProgress = () => { +export const useUserCurrentGoalStatus = () => { return useQuery({ - queryKey: [USER_GOAL_LEVEL_PROGRESS_QUERY_KEY], - queryFn: fetchUserGoalLevelProgress + queryKey: [USER_ACTIVE_GOAL_LEVEL_QUERY_KEY], + queryFn: fetchUserCurrentGoalStatus, }); }; -const createCustomGoalLevelTarget = async ( - customGoalLevelTarget: CustomGoalLevelTarget[] -): Promise> => { +const setUserGoalLevel = async ( + userGoalLevelRequest: SetUserGoalLevelRequest +): Promise> => { const response = await api.post<{ message: string; - data: CustomGoalLevelTargetResponse[]; - }>( - `${BACKEND_URL}/api/v1/user/goal/level/custom/targets`, - customGoalLevelTarget - ); + data: UserGoalLevelStatus; + }>(`${BACKEND_URL}/api/v1/user/goal/level`, userGoalLevelRequest); return response.data; }; -export const useCustomGoalLevelTarget = () => { +export const useSetUserGoalLevel = () => { return useMutation({ - mutationFn: (customGoalLevelTarget: CustomGoalLevelTarget[]) => - createCustomGoalLevelTarget(customGoalLevelTarget) + mutationFn: (userGoalLevelRequest: SetUserGoalLevelRequest) => + setUserGoalLevel(userGoalLevelRequest) }); }; +const resetUserGoalStatus = async (): Promise> => { + const response = await api.post<{ + message: string; + data: UserGoal; + }>(`${BACKEND_URL}/api/v1/user/goal/level/reset`); + + return response.data; +}; + +export const useResetUserGoalStatus = () => { + return useMutation({ + mutationFn: () => resetUserGoalStatus(), + },); +}; + const fetchAllContributionTypes = async (): Promise< ApiResponse > => { diff --git a/frontend/src/features/Admin/AdminLogin.tsx b/frontend/src/features/Admin/AdminLogin.tsx index 164b8036..12f10c8c 100644 --- a/frontend/src/features/Admin/AdminLogin.tsx +++ b/frontend/src/features/Admin/AdminLogin.tsx @@ -1,19 +1,18 @@ -import { type FC } from "react"; +import { type FC, useState } from "react"; import type { AdminCredentials } from "@/shared/types/types"; import { useLogInAdmin } from "@/api/queries/Admin"; import { useForm } from "react-hook-form"; import { useNavigate } from "react-router-dom"; import { ACCESS_TOKEN_KEY } from "@/shared/constants/local-storage"; import { Button } from "@/shared/components/ui/button"; +import { toast } from "sonner"; +import { Eye, EyeOff } from "lucide-react"; const AdminLogin: FC = () => { - const { - register, - handleSubmit, - formState: { errors } - } = useForm(); + const { register, handleSubmit } = useForm(); const navigate = useNavigate(); - const { mutate, isPending, isError, error } = useLogInAdmin(); + const { mutate, isPending } = useLogInAdmin(); + const [showPassword, setShowPassword] = useState(false); const onSubmit = async ( data: AdminCredentials, @@ -29,11 +28,12 @@ const AdminLogin: FC = () => { navigate("/admin/users"); }, onError: err => { - console.error(" Admin login error", err); + toast.error("Invalid credentials"); + console.error("Admin login error", err); } }); } catch (err) { - console.error(" Unexpected submit error", err); + console.error("Unexpected submit error", err); } }; @@ -47,17 +47,15 @@ const AdminLogin: FC = () => { htmlFor="email" className="block text-sm font-medium text-gray-700" > - Email + Email * - {errors.email && ( -

{errors.email.message}

- )}
@@ -65,25 +63,26 @@ const AdminLogin: FC = () => { htmlFor="password" className="block text-sm font-medium text-gray-700" > - Password + Password * - - {errors.password && ( -

{errors.password.message}

- )} +
+ + +
- {isError && ( -

- {error?.message || "Failed to log in"} -

- )} -
diff --git a/frontend/src/features/Login/components/LoginComponent.tsx b/frontend/src/features/Login/components/LoginComponent.tsx index f0d48276..4d26387a 100644 --- a/frontend/src/features/Login/components/LoginComponent.tsx +++ b/frontend/src/features/Login/components/LoginComponent.tsx @@ -51,7 +51,7 @@ const LoginComponent = () => { diff --git a/frontend/src/features/MyContributions/components/Repositories.tsx b/frontend/src/features/MyContributions/components/Repositories.tsx index ccc575b9..e892ebc8 100644 --- a/frontend/src/features/MyContributions/components/Repositories.tsx +++ b/frontend/src/features/MyContributions/components/Repositories.tsx @@ -13,20 +13,30 @@ const Repositories = () => { ); return (
- {repositoriesData?.map(repo => ( - <> - - - - ))} + {repositoriesData?.length === 0 ? ( +
+

+ You haven't contributed to any repositories yet +

+

+ Start contributing to earn coins and track your progress +

+
+ ) : ( + repositoriesData?.map(repo => ( +
+ + +
+ )) + )}
); }; diff --git a/frontend/src/features/MyContributions/index.tsx b/frontend/src/features/MyContributions/index.tsx index c2b31707..468ad285 100644 --- a/frontend/src/features/MyContributions/index.tsx +++ b/frontend/src/features/MyContributions/index.tsx @@ -1,7 +1,18 @@ import Repositories from "./components/Repositories"; const MyContributions = () => { - return ; + return ( +
+
+ + My Contributed Repositories + +
+
+ +
+
+ ); }; export default MyContributions; diff --git a/frontend/src/features/RepositoryDetails/components/ContributorsCard.tsx b/frontend/src/features/RepositoryDetails/components/ContributorsCard.tsx index 883224a0..f1fb3692 100644 --- a/frontend/src/features/RepositoryDetails/components/ContributorsCard.tsx +++ b/frontend/src/features/RepositoryDetails/components/ContributorsCard.tsx @@ -25,7 +25,7 @@ const ContributorsCard: FC = ({ className="h-15 w-15 rounded-full" /> Contributors-Image -
+
{name}
{contributions} Contributions
diff --git a/frontend/src/features/RepositoryDetails/components/RepositoryCard.tsx b/frontend/src/features/RepositoryDetails/components/RepositoryCard.tsx index a0d4365e..04945440 100644 --- a/frontend/src/features/RepositoryDetails/components/RepositoryCard.tsx +++ b/frontend/src/features/RepositoryDetails/components/RepositoryCard.tsx @@ -21,17 +21,22 @@ const RepositoryCard: FC = ({ return (
-
- {name} - - - -
+ +
+ {name} + + +
+
+

Owned By: {owner}

diff --git a/frontend/src/features/RepositoryDetails/index.tsx b/frontend/src/features/RepositoryDetails/index.tsx index 953dadff..85dcfce5 100644 --- a/frontend/src/features/RepositoryDetails/index.tsx +++ b/frontend/src/features/RepositoryDetails/index.tsx @@ -9,17 +9,17 @@ import { Separator } from "@/shared/components/ui/separator"; const RepositoryDetails = () => { const navigate = useNavigate(); return ( -
-
+
+
navigate("/my-contributions")} + > - navigate("/my-contributions")} - > + Repository Details
-
+
diff --git a/frontend/src/shared/components/UserDashboard/UserEmail.tsx b/frontend/src/shared/components/UserDashboard/UserEmail.tsx index e876d0a3..c291dc78 100644 --- a/frontend/src/shared/components/UserDashboard/UserEmail.tsx +++ b/frontend/src/shared/components/UserDashboard/UserEmail.tsx @@ -9,6 +9,9 @@ import { Input } from "@/shared/components/ui/input"; import { Button } from "@/shared/components/ui/button"; import { useState } from "react"; import { useUpdateUserEmail } from "@/api/queries/UserProfileDetails"; +import { queryClient } from "@/api/react-query"; +import { LOGGED_IN_USER_QUERY_KEY } from "@/shared/constants/query-keys"; +import { toast } from "sonner"; interface Props { defaultEmail: string; @@ -25,7 +28,13 @@ const UserEmail = ({ defaultEmail, onClose }: Props) => { const handleUpdate = () => { if (!isValidEmail(email)) return; updateEmail(email, { - onSuccess: () => onClose() + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: [LOGGED_IN_USER_QUERY_KEY] + }); + toast.success("email updated successfully"); + onClose(); + } }); }; @@ -36,6 +45,8 @@ const UserEmail = ({ defaultEmail, onClose }: Props) => { Update Email + We need your email, to send you notifications + { /> - diff --git a/frontend/src/shared/components/UserDashboard/UserGoals.tsx b/frontend/src/shared/components/UserDashboard/UserGoals.tsx index b6713868..bdec2968 100644 --- a/frontend/src/shared/components/UserDashboard/UserGoals.tsx +++ b/frontend/src/shared/components/UserDashboard/UserGoals.tsx @@ -12,24 +12,22 @@ import { import { Loader2 } from "lucide-react"; import { useAllContributionTypes, - useCustomGoalLevelTarget, useGoalLevels, + useResetUserGoalStatus, useSetUserGoalLevel, - useUserActiveGoalLevel, - useUserGoalLevelProgress + useUserCurrentGoalStatus } from "@/api/queries/UserGoals"; import { useQueryClient } from "@tanstack/react-query"; -import { - USER_ACTIVE_GOAL_LEVEL_QUERY_KEY, - USER_GOAL_LEVEL_PROGRESS_QUERY_KEY -} from "@/shared/constants/query-keys"; +import { USER_ACTIVE_GOAL_LEVEL_QUERY_KEY } from "@/shared/constants/query-keys"; import type { ContributionTypeDetail, CustomGoalLevelTarget } from "@/shared/types/types"; +import { toast } from "sonner"; const UserGoals = () => { const [dialogOpen, setDialogOpen] = useState(false); + const [resetDialogOpen, setResetDialogOpen] = useState(false); const [isSettingLevel, setIsSettingLevel] = useState(false); const [isCustomDialogOpen, setIsCustomDialogOpen] = useState(false); @@ -38,46 +36,74 @@ const UserGoals = () => { const [target, setTarget] = useState(""); const { data: userGoalLevelRes, isLoading: isGoalLevelLoading } = - useUserActiveGoalLevel(); + useUserCurrentGoalStatus(); const { data: goalLevelsRes, isLoading: isGoalLevelsLoading } = useGoalLevels(); - const { data: userProgressRes, isLoading: isProgressLoading } = - useUserGoalLevelProgress(); const { mutate: setGoalLevel } = useSetUserGoalLevel(); + const { mutate: resetGoalStatus } = useResetUserGoalStatus(); const { data: contributionTypesRes } = useAllContributionTypes(); - const { mutate: setCustomTarget, isPending: isSettingCustom } = - useCustomGoalLevelTarget(); const queryClient = useQueryClient(); - const userLevel = userGoalLevelRes?.data ?? ""; + const userLevel = userGoalLevelRes?.data ?? null; const goalLevels = goalLevelsRes?.data ?? []; - const userProgress = userProgressRes?.data ?? []; const allTypes: ContributionTypeDetail[] = contributionTypesRes?.data ?? []; + const createdAt = userLevel?.createdAt + ? new Date(userLevel?.createdAt) + : null; + + const isWithin48HoursOrGreaterThan30Days = (createdAt?: Date | null) => { + if (!createdAt) return false; + const diff = Date.now() - createdAt.getTime(); + return diff < 48 * 60 * 60 * 1000 || diff > 30 * 24 * 60 * 60 * 1000; + }; + const handleLevelSelect = (level: string) => { setIsSettingLevel(true); - setGoalLevel(level, { - onSuccess: () => { - queryClient.invalidateQueries({ - queryKey: [USER_ACTIVE_GOAL_LEVEL_QUERY_KEY] - }); - queryClient.invalidateQueries({ - queryKey: [USER_GOAL_LEVEL_PROGRESS_QUERY_KEY] - }); - setIsSettingLevel(false); + if (level.toLowerCase() === "custom") { + setDialogOpen(false); + setIsCustomDialogOpen(true); + setIsSettingLevel(false); + return; + } - if (level.toLowerCase() === "custom") { - setDialogOpen(false); // close default dialog - setIsCustomDialogOpen(true); // open custom dialog - } else { + setGoalLevel( + { level, customTargets: [] }, + { + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: [USER_ACTIVE_GOAL_LEVEL_QUERY_KEY] + }); + toast.success("goal set successfully"); + setIsSettingLevel(false); setDialogOpen(false); + }, + onError: () => { + toast.error("Failed to set user goal level"); + setIsSettingLevel(false); } + } + ); + }; + + const handleGoalReset = () => { + resetGoalStatus(undefined, { + onSuccess: () => { + queryClient.removeQueries({ + queryKey: [USER_ACTIVE_GOAL_LEVEL_QUERY_KEY] + }); + queryClient.refetchQueries({ + queryKey: [USER_ACTIVE_GOAL_LEVEL_QUERY_KEY] + }); + toast.success("goal reset successfully"); + setResetDialogOpen(false); }, - onError: () => { - console.error("Failed to set user goal level"); - setIsSettingLevel(false); + onError: (err: any) => { + const message = + err?.response?.data?.message || "Failed to reset goal status"; + toast.error(message); } }); }; @@ -85,37 +111,32 @@ const UserGoals = () => { const handleAddCustomGoal = () => { if (!selectedType || !target) return; if (customGoals.some(g => g.contributionType === selectedType)) return; - setCustomGoals(prev => [ ...prev, - { - contributionType: selectedType, - target: Number(target) - } + { contributionType: selectedType, target: Number(target) } ]); setSelectedType(""); setTarget(""); }; const handleSubmitCustomGoals = () => { - setCustomTarget(customGoals, { - onSuccess: () => { - queryClient.invalidateQueries({ - queryKey: [USER_ACTIVE_GOAL_LEVEL_QUERY_KEY] - }); - queryClient.invalidateQueries({ - queryKey: [USER_GOAL_LEVEL_PROGRESS_QUERY_KEY] - }); - setIsCustomDialogOpen(false); - setCustomGoals([]); - }, - onError: () => { - console.error("Failed to set custom goal targets"); + setGoalLevel( + { level: "Custom", customTargets: customGoals }, + { + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: [USER_ACTIVE_GOAL_LEVEL_QUERY_KEY] + }); + toast.success("goal set successfully"); + setIsCustomDialogOpen(false); + setCustomGoals([]); + }, + onError: () => toast.error("Failed to set custom goal targets") } - }); + ); }; - if (isGoalLevelLoading || isGoalLevelsLoading || isProgressLoading) { + if (isGoalLevelLoading || isGoalLevelsLoading) { return (
@@ -126,25 +147,56 @@ const UserGoals = () => { return (
-

- MY GOALS {userLevel && `(${userLevel.toUpperCase()})`} -

+
+

+ MY GOALS {userLevel?.level && `(${userLevel.level.toUpperCase()})`} +

+ {isWithin48HoursOrGreaterThan30Days(createdAt) && ( + setResetDialogOpen(open)} + > + + + + + + Confirm Goal Reset + +

+ - Reset is available within 48 hours of setting a goal. +
- After 48 hours, goals reset automatically after 30 days. +

+ + + + +
+
+ )} +
{userLevel ? (
- {userProgress.map((goal, index) => { - const percent = goal.targetCount - ? Math.min((goal.achievedCount / goal.targetCount) * 100, 100) + {userLevel.goalTargetProgress?.map((goal, idx) => { + const percent = goal.target + ? Math.min((goal.progress / goal.target) * 100, 100) : 0; - return ( -
+
{goal.contributionType.replace(/([A-Z])/g, " $1")} - {goal.achievedCount}/{goal.targetCount} + {goal.progress}/{goal.target}
{ })}
) : ( -
+

No Active Goal Set

-

- You haven't selected a goal level for this month yet. Choose a level - to start tracking your contributions. +

+ You haven't selected a goal level yet. Choose a level to start + tracking contributions.

- - + - Select Goal Level for the month + Select Goal Level {!isSettingLevel ? ( @@ -179,7 +230,7 @@ const UserGoals = () => {
)} + {/* Custom Goal Dialog */} - + Set Custom Contribution Goals @@ -244,7 +296,7 @@ const UserGoals = () => { {customGoals.map((goal, idx) => (
{goal.contributionType} {goal.target} @@ -265,16 +317,10 @@ const UserGoals = () => { - Settings + + Settings + - Logout + + Logout + ); diff --git a/frontend/src/shared/components/ui/button.tsx b/frontend/src/shared/components/ui/button.tsx index 8a384cab..254d133b 100644 --- a/frontend/src/shared/components/ui/button.tsx +++ b/frontend/src/shared/components/ui/button.tsx @@ -10,18 +10,18 @@ const buttonVariants = cva( variants: { variant: { primary: - "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", + "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90 hover:cursor-pointer", destructive: - "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 hover:cursor-pointer", outline: "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 hover:cursor-pointer", secondary: - "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", + "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80 hover:cursor-pointer", ghost: - "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", - link: "text-primary underline-offset-4 hover:underline", + "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50 hover:cursor-pointer", + link: "text-primary underline-offset-4 hover:underline hover:cursor-pointer", ccAppOutlineMidBlue: - "bg-cc-app-mid-blue hover:bg-cc-app-blue rounded-sm border border-white text-white hover:cursor-pointer", + "bg-cc-app-mid-blue hover:bg-cc-app-blue rounded-sm border border-white text-white hover:cursor-pointer hover:cursor-pointer", ccAppOutline: "border-cc-app-mid-blue text-cc-app-blue hover:bg-cc-app-mid-blue/5 rounded-sm border focus:outline-none hover:cursor-pointer", ccAppOutlineRed: diff --git a/frontend/src/shared/constants/local-storage.ts b/frontend/src/shared/constants/local-storage.ts index 964b3b29..7d1daf7c 100644 --- a/frontend/src/shared/constants/local-storage.ts +++ b/frontend/src/shared/constants/local-storage.ts @@ -1 +1,2 @@ export const ACCESS_TOKEN_KEY = "cc-7db23e66-accessToken"; +export const USER_DATA_KEY="userData" diff --git a/frontend/src/shared/constants/query-keys.ts b/frontend/src/shared/constants/query-keys.ts index 5789c00e..780b651c 100644 --- a/frontend/src/shared/constants/query-keys.ts +++ b/frontend/src/shared/constants/query-keys.ts @@ -14,5 +14,6 @@ export const GOAL_LEVELS_QUERY_KEY = "goal-levels"; export const USER_GOAL_LEVEL_PROGRESS_QUERY_KEY = "goal-level-progresss"; export const CONTRIBUTION_TYPES_QUERY_KEY = "contribution-types"; export const GITHUB_OAUTH_LOGIN_QUERY_KEY = "github-oauth-login"; +export const USER_GOAL_LEVEL_UPDATE_QUERY_KEY = "user-goal-level-update"; -export const GET_ALL_USERS_QUERY_KEY = "get-all-users"; +export const GET_ALL_USERS_QUERY_KEY = "get-all-users"; \ No newline at end of file diff --git a/frontend/src/shared/layout/AdminLayout.tsx b/frontend/src/shared/layout/AdminLayout.tsx index 98ff8f87..530bce27 100644 --- a/frontend/src/shared/layout/AdminLayout.tsx +++ b/frontend/src/shared/layout/AdminLayout.tsx @@ -64,7 +64,7 @@ const AdminLayout: FC = ({ children }) => { - - - Select Goal Level + + + + Select Goal Level + - {!isSettingLevel ? ( -
+
{goalLevels.map(level => (
) : ( -
- - Setting your goal... +
+ + Setting your goal...
)} - - - diff --git a/frontend/src/shared/layout/AppLayout.tsx b/frontend/src/shared/layout/AppLayout.tsx new file mode 100644 index 00000000..fd69e585 --- /dev/null +++ b/frontend/src/shared/layout/AppLayout.tsx @@ -0,0 +1,17 @@ +import { type ReactNode } from "react"; + +interface AppLayoutProps { + children: ReactNode; +} + +const AppLayout = ({ children }: AppLayoutProps) => { + return ( +
+
+ {children} +
+
+ ); +}; + +export default AppLayout; From 09293420fbada792ea4e085a6b9864f5a7067c31 Mon Sep 17 00:00:00 2001 From: VishakhaSainani-Josh Date: Thu, 4 Sep 2025 12:37:55 +0530 Subject: [PATCH 26/36] fix goalcompletion status --- backend/internal/app/goal/service.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/internal/app/goal/service.go b/backend/internal/app/goal/service.go index 6f1a1c01..40548239 100644 --- a/backend/internal/app/goal/service.go +++ b/backend/internal/app/goal/service.go @@ -264,7 +264,7 @@ func (s *service) GetUserCurrentGoalStatus(ctx context.Context, userId int) (*Ge Progress: contributionProgressCount, } - if goalTargetProgress.Target == goalTargetProgress.Progress { + if goalTargetProgress.Target <= goalTargetProgress.Progress { totalTargetsCompleted++ } @@ -281,7 +281,7 @@ func (s *service) GetUserCurrentGoalStatus(ctx context.Context, userId int) (*Ge GoalTargetProgress: goalTargetProgresses, } - if totalTargets == totalTargetsCompleted { + if totalTargets <= totalTargetsCompleted { userGoal := UserGoal{ Id: userCurrentGoal.Id, Status: GoalStatusCompleted, @@ -341,12 +341,12 @@ func (s *service) AllocateBadge(ctx context.Context, userId int) error { var totalTargetsCompleted int totalTargets := len(userCurrentGoalStatus.GoalTargetProgress) for _, goalTargetProgress := range userCurrentGoalStatus.GoalTargetProgress { - if goalTargetProgress.Progress == goalTargetProgress.Target { + if goalTargetProgress.Target <= goalTargetProgress.Progress { totalTargetsCompleted++ } } - if totalTargets == totalTargetsCompleted { + if totalTargets <= totalTargetsCompleted { userGoal := UserGoal{ Id: userCurrentGoalStatus.UserGoalId, Status: GoalStatusCompleted, From 4e953c780571904ae882dada845eaa5ef62893bf Mon Sep 17 00:00:00 2001 From: VishakhaSainani-Josh Date: Thu, 4 Sep 2025 12:38:25 +0530 Subject: [PATCH 27/36] fix ui --- .gitignore | 5 +-- frontend/src/assets/customBadge.svg | 42 +++++++++++++++++++ .../UserDashboard/components/OverviewCard.tsx | 4 +- .../components/UserDashboard/UserBadges.tsx | 12 ++++-- .../shared/components/common/ActivityCard.tsx | 3 +- 5 files changed, 55 insertions(+), 11 deletions(-) create mode 100644 frontend/src/assets/customBadge.svg diff --git a/.gitignore b/.gitignore index 3df73e6d..ab66fe80 100644 --- a/.gitignore +++ b/.gitignore @@ -25,7 +25,4 @@ dist-ssr *.njsproj *.sln *.sw? -backend/secrets/*.json -frontend/Dockerfile -frontend/nginx.conf -frontend/schema.sql + diff --git a/frontend/src/assets/customBadge.svg b/frontend/src/assets/customBadge.svg new file mode 100644 index 00000000..a04abebc --- /dev/null +++ b/frontend/src/assets/customBadge.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/features/UserDashboard/components/OverviewCard.tsx b/frontend/src/features/UserDashboard/components/OverviewCard.tsx index 81e19767..a0652298 100644 --- a/frontend/src/features/UserDashboard/components/OverviewCard.tsx +++ b/frontend/src/features/UserDashboard/components/OverviewCard.tsx @@ -12,7 +12,9 @@ const OverviewCard: FC = ({ type, count, totalCoins }) => { return (
-

{type}

+

+ {type.replace(/([A-Z])/g, " $1")} +

{count} diff --git a/frontend/src/shared/components/UserDashboard/UserBadges.tsx b/frontend/src/shared/components/UserDashboard/UserBadges.tsx index e42547fe..9a920266 100644 --- a/frontend/src/shared/components/UserDashboard/UserBadges.tsx +++ b/frontend/src/shared/components/UserDashboard/UserBadges.tsx @@ -3,11 +3,13 @@ import type { Badge } from "@/shared/types/types"; import bronzeBadge from "@/assets/bronzeBadge.svg"; import silverBadge from "@/assets/silverBadge.svg"; import goldBadge from "@/assets/goldBadge.svg"; +import customBadge from "@/assets/customBadge.svg"; const badgeColorMap: Record = { BEGINNER: bronzeBadge, INTERMEDIATE: silverBadge, - ADVANCED: goldBadge + ADVANCED: goldBadge, + CUSTOM: customBadge }; const UserBadges = () => { @@ -36,10 +38,12 @@ const UserBadges = () => { > Badge {badgeList.length > 1 && ( - Ă—{badgeList.length} + + Ă—{badgeList.length} + )} -
- {type}
+
+ {type}
); diff --git a/frontend/src/shared/components/common/ActivityCard.tsx b/frontend/src/shared/components/common/ActivityCard.tsx index e3c87ff1..ad15b4e4 100644 --- a/frontend/src/shared/components/common/ActivityCard.tsx +++ b/frontend/src/shared/components/common/ActivityCard.tsx @@ -34,8 +34,7 @@ const ActivityCard: FC = ({
{isRepositoryActivity ? null : (
- Contributed to - <{repositoryName}> + Contributed to <{repositoryName}>
)} From a77eb7905d8d300efc23a4f7440a52d118dc7813 Mon Sep 17 00:00:00 2001 From: VishakhaSainani-Josh Date: Thu, 4 Sep 2025 13:09:04 +0530 Subject: [PATCH 28/36] implement admin leaderboard --- backend/internal/app/user/handler.go | 15 ++++++++++++++- backend/internal/repository/user.go | 2 ++ frontend/src/features/Admin/AdminLeaderboard.tsx | 11 +++++++++++ frontend/src/features/Admin/AdminLogin.tsx | 6 +++++- .../UserDashboard/components/Leaderboard.tsx | 6 +++++- frontend/src/root/routes-config.tsx | 9 +++++++++ frontend/src/shared/constants/routes.ts | 1 + frontend/src/shared/layout/AdminLayout.tsx | 3 ++- 8 files changed, 49 insertions(+), 4 deletions(-) create mode 100644 frontend/src/features/Admin/AdminLeaderboard.tsx diff --git a/backend/internal/app/user/handler.go b/backend/internal/app/user/handler.go index 02f03b37..eda941cf 100644 --- a/backend/internal/app/user/handler.go +++ b/backend/internal/app/user/handler.go @@ -20,7 +20,6 @@ type Handler interface { SoftDeleteUser(w http.ResponseWriter, r *http.Request) ListUserRanks(w http.ResponseWriter, r *http.Request) GetCurrentUserRank(w http.ResponseWriter, r *http.Request) - // UpdateCurrentActiveGoalId(w http.ResponseWriter, r *http.Request) ListAllUsers(w http.ResponseWriter, r *http.Request) BlockOrUnblockUser(w http.ResponseWriter, r *http.Request) } @@ -111,6 +110,20 @@ func (h *handler) GetCurrentUserRank(w http.ResponseWriter, r *http.Request) { return } + isAdminValue := ctx.Value(middleware.IsAdminKey) + isAdmin, ok := isAdminValue.(bool) + if !ok { + slog.Error("error obtaining id admin from context") + status, errorMessage := apperrors.MapError(apperrors.ErrContextValue) + response.WriteJson(w, status, errorMessage, nil) + return + } + + if isAdmin { + response.WriteJson(w, http.StatusOK, "current user is admin", nil) + return + } + currentUserRank, err := h.userService.GetCurrentUserRank(ctx, userId) if err != nil { slog.Error("failed to get current user rank", "error", err) diff --git a/backend/internal/repository/user.go b/backend/internal/repository/user.go index 1df72b57..6a1c4052 100644 --- a/backend/internal/repository/user.go +++ b/backend/internal/repository/user.go @@ -89,6 +89,8 @@ const ( current_balance, RANK() OVER (ORDER BY current_balance DESC) AS rank FROM users + WHERE is_admin=false + AND is_deleted=false ) ranked_users WHERE id = $1;` diff --git a/frontend/src/features/Admin/AdminLeaderboard.tsx b/frontend/src/features/Admin/AdminLeaderboard.tsx new file mode 100644 index 00000000..8f63c6d4 --- /dev/null +++ b/frontend/src/features/Admin/AdminLeaderboard.tsx @@ -0,0 +1,11 @@ +import Leaderboard from "../UserDashboard/components/Leaderboard"; + +const AdminLeaderboard = () => { + return ( +
+ +
+ ); +}; + +export default AdminLeaderboard; diff --git a/frontend/src/features/Admin/AdminLogin.tsx b/frontend/src/features/Admin/AdminLogin.tsx index 12f10c8c..a99d4d0b 100644 --- a/frontend/src/features/Admin/AdminLogin.tsx +++ b/frontend/src/features/Admin/AdminLogin.tsx @@ -3,7 +3,10 @@ import type { AdminCredentials } from "@/shared/types/types"; import { useLogInAdmin } from "@/api/queries/Admin"; import { useForm } from "react-hook-form"; import { useNavigate } from "react-router-dom"; -import { ACCESS_TOKEN_KEY } from "@/shared/constants/local-storage"; +import { + ACCESS_TOKEN_KEY, + USER_DATA_KEY +} from "@/shared/constants/local-storage"; import { Button } from "@/shared/components/ui/button"; import { toast } from "sonner"; import { Eye, EyeOff } from "lucide-react"; @@ -25,6 +28,7 @@ const AdminLogin: FC = () => { onSuccess: res => { console.log(" Admin login success", res); localStorage.setItem(ACCESS_TOKEN_KEY, res.data.jwtToken); + localStorage.setItem(USER_DATA_KEY, JSON.stringify(res.data)); navigate("/admin/users"); }, onError: err => { diff --git a/frontend/src/features/UserDashboard/components/Leaderboard.tsx b/frontend/src/features/UserDashboard/components/Leaderboard.tsx index b1482e45..ce9e589c 100644 --- a/frontend/src/features/UserDashboard/components/Leaderboard.tsx +++ b/frontend/src/features/UserDashboard/components/Leaderboard.tsx @@ -6,6 +6,7 @@ import { Card } from "@/shared/components/ui/card"; import LeaderboardCard from "@/features/UserDashboard/components/LeaderboardCard"; import { useCurrentUserRank, useLeaderboard } from "@/api/queries/Leaderboard"; import { TrendingUp } from "lucide-react"; +import { USER_DATA_KEY } from "@/shared/constants/local-storage"; interface LeaderboardProps { className?: string; @@ -22,10 +23,13 @@ const Leaderboard: FC = ({ className }) => { const leaderboard = data?.data ?? []; const leaderboardData = viewAll ? leaderboard : leaderboard?.slice(0, 10); + const user = JSON.parse(localStorage.getItem(USER_DATA_KEY) || "{}"); const { data: userData } = useCurrentUserRank(); const currentUser = userData?.data; + console.log(user.isAdmin); + return ( = ({ className }) => { /> ))}
- {!viewAll && ( + {!viewAll && !user.isAdmin && (
, + isProtected: true, + layout: Layout.AdminLayout + }, { path: ACCOUNT_INFO_PATH, element: , diff --git a/frontend/src/shared/constants/routes.ts b/frontend/src/shared/constants/routes.ts index 7dc7b3b6..6864ca58 100644 --- a/frontend/src/shared/constants/routes.ts +++ b/frontend/src/shared/constants/routes.ts @@ -7,3 +7,4 @@ export const ACCOUNT_INFO_PATH = "/account/info"; export const ADMIN_LOGIN_PATH = "/admin/login"; export const ADMIN_USERS_PATH = "/admin/users"; export const ADMIN_SCORE_CONFIGURE_PATH = "/admin/configure/score"; +export const ADMIN_LEADERBOARD_PATH="/admin/leaderboard" diff --git a/frontend/src/shared/layout/AdminLayout.tsx b/frontend/src/shared/layout/AdminLayout.tsx index 530bce27..cd83f94f 100644 --- a/frontend/src/shared/layout/AdminLayout.tsx +++ b/frontend/src/shared/layout/AdminLayout.tsx @@ -19,7 +19,8 @@ const AdminLayout: FC = ({ children }) => { icon: BarChart3, path: "/admin/configure/score" }, - { name: "Users", icon: Users, path: "/admin/users" } + { name: "Users", icon: Users, path: "/admin/users" }, + { name: "View Leaderboard", icon: BarChart3, path: "/admin/leaderboard" } ]; const handleMenuClick = (itemName: string, path: string) => { From ab84fb0a3f03011733ffedf03e7e12893b0d8398 Mon Sep 17 00:00:00 2001 From: VishakhaSainani-Josh Date: Thu, 4 Sep 2025 18:29:52 +0530 Subject: [PATCH 29/36] fix summary graph --- backend/internal/app/goal/service.go | 23 ++++++++++++++-- backend/internal/repository/goal.go | 41 ++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/backend/internal/app/goal/service.go b/backend/internal/app/goal/service.go index 40548239..af9a2fdc 100644 --- a/backend/internal/app/goal/service.go +++ b/backend/internal/app/goal/service.go @@ -417,13 +417,30 @@ func (s *service) CreateUserGoalSummary(ctx context.Context, userId int) (GoalSu TargetCompleted: totalTargetCompleted, } - createdUserGoalSummary, err := s.goalRepository.CreateUserGoalSummary(ctx, nil, repository.GoalSummary(userMonthlyGoalSummary)) + userGoalSummaryForToday, err := s.goalRepository.GetUserGoalSummaryBySnapshotDate(ctx, nil, userMonthlyGoalSummary.SnapshotDate, userId) if err != nil { - slog.Error("error creating user goal summary", "error", err) + slog.Error("error fetching user goal summary for today", "error", err) return GoalSummary{}, err } - return GoalSummary(createdUserGoalSummary), nil + var userGoalSummary GoalSummary + if userGoalSummaryForToday == nil { + createdUserGoalSummary, err := s.goalRepository.CreateUserGoalSummary(ctx, nil, repository.GoalSummary(userMonthlyGoalSummary)) + if err != nil { + slog.Error("error creating user goal summary", "error", err) + return GoalSummary{}, err + } + userGoalSummary = GoalSummary(createdUserGoalSummary) + } else { + updatedUserGoalSummary, err := s.goalRepository.UpdateUserGoalSummary(ctx, nil, userGoalSummaryForToday.Id, repository.GoalSummary(userMonthlyGoalSummary)) + if err != nil { + slog.Error("error updating user goal summary", "error", err) + return GoalSummary{}, err + } + userGoalSummary = GoalSummary(updatedUserGoalSummary) + } + + return userGoalSummary, nil } func (s *service) FetchUserGoalSummary(ctx context.Context, userId int) ([]GoalSummary, error) { diff --git a/backend/internal/repository/goal.go b/backend/internal/repository/goal.go index d8e1a490..0f3fa6da 100644 --- a/backend/internal/repository/goal.go +++ b/backend/internal/repository/goal.go @@ -32,6 +32,8 @@ type GoalRepository interface { CalculateUserIncompleteGoalsUntilDay(ctx context.Context, tx *sqlx.Tx, userID int) (int, error) CreateUserGoalSummary(ctx context.Context, tx *sqlx.Tx, userGoalSummary GoalSummary) (GoalSummary, error) FetchUserGoalSummary(ctx context.Context, tx *sqlx.Tx, userId int) ([]GoalSummary, error) + GetUserGoalSummaryBySnapshotDate(ctx context.Context, tx *sqlx.Tx, snapshotDate time.Time, userId int) (*GoalSummary, error) + UpdateUserGoalSummary(ctx context.Context, tx *sqlx.Tx, goalSummaryId int, userGoalSummary GoalSummary) (GoalSummary, error) } func NewGoalRepository(db *sqlx.DB) GoalRepository { @@ -117,6 +119,10 @@ const ( RETURNING *` fetchUserGoalSummaryQuery = "SELECT * FROM goal_summary WHERE user_id=$1" + + getUserGoalSummaryBySnapshotDateQuery = "SELECT * FROM goal_summary WHERE snapshot_date<$1 AND user_id=$2" + + updateUserGoalSummaryQuery = "UPDATE user_goal SET snapshot_date=$2, incomplete_goals_count=$3, target_set=$4, target_completed=$5 where id=$1 " ) func (gr *goalRepository) ListGoalLevels(ctx context.Context, tx *sqlx.Tx) ([]GoalLevel, error) { @@ -348,3 +354,38 @@ func (gr *goalRepository) FetchUserGoalSummary(ctx context.Context, tx *sqlx.Tx, return usersGoalSummary, nil } + +func (gr *goalRepository) GetUserGoalSummaryBySnapshotDate(ctx context.Context, tx *sqlx.Tx, snapshotDate time.Time, userId int) (*GoalSummary, error) { + executer := gr.BaseRepository.initiateQueryExecuter(tx) + + var userGoalSummary GoalSummary + err := executer.GetContext(ctx, &userGoalSummary, getUserGoalSummaryBySnapshotDateQuery, snapshotDate, userId) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, nil + } + slog.Error("error getting user goal summary", "error", err) + return nil, err + } + + return &userGoalSummary, nil +} + +func (gr *goalRepository) UpdateUserGoalSummary(ctx context.Context, tx *sqlx.Tx, goalSummaryId int, userGoalSummary GoalSummary) (GoalSummary, error) { + executer := gr.BaseRepository.initiateQueryExecuter(tx) + + var updatedUserGoalSummary GoalSummary + err := executer.GetContext(ctx, &updatedUserGoalSummary, updateUserGoalSummaryQuery, + goalSummaryId, + userGoalSummary.SnapshotDate, + userGoalSummary.IncompleteGoalsCount, + userGoalSummary.TargetSet, + userGoalSummary.TargetCompleted, + ) + if err != nil { + slog.Error("failed to update user goal summary", "error", err) + return GoalSummary{}, apperrors.ErrInternalServer + } + + return GoalSummary{}, nil +} From 1f7bffe72fa7e4f448d545be2d32588a036ff153 Mon Sep 17 00:00:00 2001 From: VishakhaSainani-Josh Date: Thu, 4 Sep 2025 18:30:36 +0530 Subject: [PATCH 30/36] enhance ui --- .../src/features/Admin/AdminLeaderboard.tsx | 2 +- frontend/src/features/Admin/Users.tsx | 442 ++++++++++++------ .../components/Repositories.tsx | 79 +++- .../src/features/MyContributions/index.tsx | 7 +- .../UserDashboard/components/Leaderboard.tsx | 13 +- .../components/UserDashboardComponent.tsx | 2 +- frontend/src/shared/components/ui/button.tsx | 4 +- frontend/src/shared/components/ui/table.tsx | 120 +++++ frontend/src/shared/layout/AdminLayout.tsx | 4 +- frontend/src/shared/layout/AppLayout.tsx | 2 +- 10 files changed, 518 insertions(+), 157 deletions(-) create mode 100644 frontend/src/shared/components/ui/table.tsx diff --git a/frontend/src/features/Admin/AdminLeaderboard.tsx b/frontend/src/features/Admin/AdminLeaderboard.tsx index 8f63c6d4..54fc93b4 100644 --- a/frontend/src/features/Admin/AdminLeaderboard.tsx +++ b/frontend/src/features/Admin/AdminLeaderboard.tsx @@ -3,7 +3,7 @@ import Leaderboard from "../UserDashboard/components/Leaderboard"; const AdminLeaderboard = () => { return (
- +
); }; diff --git a/frontend/src/features/Admin/Users.tsx b/frontend/src/features/Admin/Users.tsx index 9ae0e514..9725e630 100644 --- a/frontend/src/features/Admin/Users.tsx +++ b/frontend/src/features/Admin/Users.tsx @@ -1,5 +1,12 @@ import { type FC, useEffect, useState } from "react"; -import { Card } from "@/shared/components/ui/card"; +import { + type ColumnDef, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + useReactTable +} from "@tanstack/react-table"; import defaultAvatar from "@/assets/default-profile-pic.svg"; import { Loader, @@ -7,20 +14,45 @@ import { Shield, ShieldOff, ChevronLeft, - ChevronRight + ChevronRight, + ChevronsLeft, + ChevronsRight, + Search, + X } from "lucide-react"; import { useGetAllUsers, useUpdateUserBlockStatus } from "@/api/queries/Admin"; import Coin from "@/shared/components/common/Coin"; import { toast } from "sonner"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow +} from "@/shared/components/ui/table"; +import { Button } from "@/shared/components/ui/button"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from "@/shared/components/ui/select"; +import { Input } from "@/shared/components/ui/input"; -const ITEMS_PER_PAGE = 10; +interface User { + userId: number; + githubUsername: string; + avatarUrl?: string; + currentBalance: number; + isBlocked: boolean; +} export const AllUsersList: FC = () => { const { data, isLoading } = useGetAllUsers(); const { mutate: updateBlockStatus } = useUpdateUserBlockStatus(); - - const [currentPage, setCurrentPage] = useState(1); - const [users, setUsers] = useState(data?.data || []); + const [users, setUsers] = useState(data?.data || []); useEffect(() => { if (data?.data) { @@ -28,11 +60,6 @@ export const AllUsersList: FC = () => { } }, [data]); - const totalPages = Math.ceil(users.length / ITEMS_PER_PAGE); - const startIndex = (currentPage - 1) * ITEMS_PER_PAGE; - const endIndex = startIndex + ITEMS_PER_PAGE; - const currentUsers = users.slice(startIndex, endIndex); - const handleBlockToggle = (userId: number, block: boolean) => { updateBlockStatus( { userId, block }, @@ -54,9 +81,99 @@ export const AllUsersList: FC = () => { ); }; - const goToPage = (page: number) => { - setCurrentPage(Math.max(1, Math.min(page, totalPages))); - }; + const columns: ColumnDef[] = [ + { + accessorKey: "githubUsername", + header: "User", + cell: ({ row }) => { + const user = row.original; + return ( +
+ User Avatar + + {user.githubUsername} + +
+ ); + } + }, + { + accessorKey: "currentBalance", + header: "Balance", + cell: ({ row }) => { + return ( +
+ + {row.getValue("currentBalance")} +
+ ); + } + }, + { + accessorKey: "isBlocked", + header: "Status", + cell: ({ row }) => { + const isBlocked = row.getValue("isBlocked") as boolean; + return ( + + {isBlocked ? ( + <> + + Blocked + + ) : ( + <> + + Active + + )} + + ); + } + }, + { + id: "actions", + header: () =>
Actions
, + cell: ({ row }) => { + const user = row.original; + return ( +
+ +
+ ); + } + } + ]; + + const table = useReactTable({ + data: users, + columns, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getFilteredRowModel: getFilteredRowModel(), + initialState: { + pagination: { + pageSize: 10 + } + } + }); if (isLoading) { return ( @@ -79,142 +196,199 @@ export const AllUsersList: FC = () => { } return ( -
+

User Management

-

- Showing {startIndex + 1}-{Math.min(endIndex, users.length)} of{" "} - {users.length} users -

+
+

+ Showing{" "} + {table.getState().pagination.pageIndex * + table.getState().pagination.pageSize + + 1} + - + {Math.min( + (table.getState().pagination.pageIndex + 1) * + table.getState().pagination.pageSize, + table.getFilteredRowModel().rows.length + )}{" "} + of {table.getFilteredRowModel().rows.length} users + {table.getState().globalFilter && + ` (filtered from ${users.length} total)`} +

+
-
- {currentUsers.map(user => ( - -
-
-
- User Avatar -
+
+
+ + table.setGlobalFilter(e.target.value)} + className="pl-8" + /> + {table.getState().globalFilter && ( + + )} +
-
-
-

- {user.githubUsername} -

-
- - {user.currentBalance} -
-
-
- - {user.isBlocked ? ( - <> - - Blocked - - ) : ( - <> - - Active - - )} - -
-
-
+ +
-
- - -
- {Array.from({ length: Math.min(5, totalPages) }, (_, i) => { - let pageNum; - if (totalPages <= 5) { - pageNum = i + 1; - } else if (currentPage <= 3) { - pageNum = i + 1; - } else if (currentPage >= totalPages - 2) { - pageNum = totalPages - 4 + i; - } else { - pageNum = currentPage - 2 + i; - } - - return ( - - ); - })} -
- - + -
- -
- Page {currentPage} of {totalPages} + Go to previous page + + + +
- )} +
); }; diff --git a/frontend/src/features/MyContributions/components/Repositories.tsx b/frontend/src/features/MyContributions/components/Repositories.tsx index e892ebc8..fbfc5aec 100644 --- a/frontend/src/features/MyContributions/components/Repositories.tsx +++ b/frontend/src/features/MyContributions/components/Repositories.tsx @@ -1,29 +1,96 @@ +import { useState } from "react"; import { useRepositories } from "@/api/queries/Repositories"; import { Separator } from "@/shared/components/ui/separator"; import RepositoriesCard from "./RepositoriesCard"; const Repositories = () => { const { data, isLoading } = useRepositories(); - const repositoriesData = data?.data; + const repositoriesData = data?.data || []; + + const [search, setSearch] = useState(""); + const [language, setLanguage] = useState("All"); + const [sort, setSort] = useState("latest"); + if (isLoading) return (
); + + const filteredRepos = repositoriesData + .filter( + repo => + repo.repoName.toLowerCase().includes(search.toLowerCase()) || + repo.description?.toLowerCase().includes(search.toLowerCase()) || + repo.languages?.some((lang: string) => + lang.toLowerCase().includes(search.toLowerCase()) + ) + ) + .filter(repo => + language === "All" ? true : repo.languages?.includes(language) + ) + .sort((a, b) => { + if (sort === "latest") + return ( + new Date(b.updateDate).getTime() - new Date(a.updateDate).getTime() + ); + if (sort === "oldest") + return ( + new Date(a.updateDate).getTime() - new Date(b.updateDate).getTime() + ); + return 0; + }); + return ( -
- {repositoriesData?.length === 0 ? ( +
+
+ setSearch(e.target.value)} + className="border-cc-app-blue text-cc-app-blue placeholder-cc-app-blue focus:border-cc-app-blue focus:ring-cc-app-blue/70 h-10 flex-1 rounded-lg border bg-blue-50 px-4 text-sm shadow-sm transition focus:ring-2 focus:outline-none" + /> + +
+ + + +
+
+ + {filteredRepos.length === 0 ? (

- You haven't contributed to any repositories yet + No repositories found

- Start contributing to earn coins and track your progress + Try adjusting your search or filters

) : ( - repositoriesData?.map(repo => ( + filteredRepos.map(repo => (
{ return ( -
-
- - My Contributed Repositories - -
+
diff --git a/frontend/src/features/UserDashboard/components/Leaderboard.tsx b/frontend/src/features/UserDashboard/components/Leaderboard.tsx index ce9e589c..86900d64 100644 --- a/frontend/src/features/UserDashboard/components/Leaderboard.tsx +++ b/frontend/src/features/UserDashboard/components/Leaderboard.tsx @@ -6,13 +6,19 @@ import { Card } from "@/shared/components/ui/card"; import LeaderboardCard from "@/features/UserDashboard/components/LeaderboardCard"; import { useCurrentUserRank, useLeaderboard } from "@/api/queries/Leaderboard"; import { TrendingUp } from "lucide-react"; -import { USER_DATA_KEY } from "@/shared/constants/local-storage"; +import { useLocation } from "react-router-dom"; +import { ADMIN_LEADERBOARD_PATH } from "@/shared/constants/routes"; interface LeaderboardProps { className?: string; } const Leaderboard: FC = ({ className }) => { + let isAdmin = false; + const location = useLocation(); + if (location.pathname == ADMIN_LEADERBOARD_PATH) { + isAdmin = true; + } const [viewAll, setViewAll] = useState(false); const handleViewAll = () => { @@ -23,13 +29,10 @@ const Leaderboard: FC = ({ className }) => { const leaderboard = data?.data ?? []; const leaderboardData = viewAll ? leaderboard : leaderboard?.slice(0, 10); - const user = JSON.parse(localStorage.getItem(USER_DATA_KEY) || "{}"); const { data: userData } = useCurrentUserRank(); const currentUser = userData?.data; - console.log(user.isAdmin); - return ( = ({ className }) => { /> ))}
- {!viewAll && !user.isAdmin && ( + {!viewAll && !isAdmin && (
{ return ( -
+
diff --git a/frontend/src/shared/components/ui/button.tsx b/frontend/src/shared/components/ui/button.tsx index 254d133b..5fc0eaeb 100644 --- a/frontend/src/shared/components/ui/button.tsx +++ b/frontend/src/shared/components/ui/button.tsx @@ -25,7 +25,9 @@ const buttonVariants = cva( ccAppOutline: "border-cc-app-mid-blue text-cc-app-blue hover:bg-cc-app-mid-blue/5 rounded-sm border focus:outline-none hover:cursor-pointer", ccAppOutlineRed: - "border-red text-white hover:bg-red-800 rounded-sm border focus:outline-none bg-red-700 hover:cursor-pointer" + "border-red text-white hover:bg-red-800 rounded-sm border focus:outline-none bg-red-700 hover:cursor-pointer", + success: + "bg-green-600 text-white shadow-xs hover:bg-green-700 focus-visible:ring-green-500/20 dark:focus-visible:ring-green-400/40 hover:cursor-pointer" }, size: { md: "h-9 px-4 py-2 has-[>svg]:px-3", diff --git a/frontend/src/shared/components/ui/table.tsx b/frontend/src/shared/components/ui/table.tsx new file mode 100644 index 00000000..99ee356e --- /dev/null +++ b/frontend/src/shared/components/ui/table.tsx @@ -0,0 +1,120 @@ +import * as React from "react" + +import { cn } from "@/shared/utils/tailwindcss" + +const Table = React.forwardRef< + HTMLTableElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+ + +)) +Table.displayName = "Table" + +const TableHeader = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableHeader.displayName = "TableHeader" + +const TableBody = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableBody.displayName = "TableBody" + +const TableFooter = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + tr]:last:border-b-0", + className + )} + {...props} + /> +)) +TableFooter.displayName = "TableFooter" + +const TableRow = React.forwardRef< + HTMLTableRowElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableRow.displayName = "TableRow" + +const TableHead = React.forwardRef< + HTMLTableCellElement, + React.ThHTMLAttributes +>(({ className, ...props }, ref) => ( +
[role=checkbox]]:translate-y-[2px]", + className + )} + {...props} + /> +)) +TableHead.displayName = "TableHead" + +const TableCell = React.forwardRef< + HTMLTableCellElement, + React.TdHTMLAttributes +>(({ className, ...props }, ref) => ( + [role=checkbox]]:translate-y-[2px]", + className + )} + {...props} + /> +)) +TableCell.displayName = "TableCell" + +const TableCaption = React.forwardRef< + HTMLTableCaptionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +TableCaption.displayName = "TableCaption" + +export { + Table, + TableHeader, + TableBody, + TableFooter, + TableHead, + TableRow, + TableCell, + TableCaption, +} diff --git a/frontend/src/shared/layout/AdminLayout.tsx b/frontend/src/shared/layout/AdminLayout.tsx index cd83f94f..ba043533 100644 --- a/frontend/src/shared/layout/AdminLayout.tsx +++ b/frontend/src/shared/layout/AdminLayout.tsx @@ -1,5 +1,5 @@ import React, { type FC, type ReactNode } from "react"; -import { User, BarChart3, Users, ChevronRight, LogOut } from "lucide-react"; +import { User, BarChart3, Users, ChevronRight, LogOut, Trophy } from "lucide-react"; import { useNavigate } from "react-router-dom"; import { ADMIN_LOGIN_PATH } from "../constants/routes"; import { Button } from "../components/ui/button"; @@ -20,7 +20,7 @@ const AdminLayout: FC = ({ children }) => { path: "/admin/configure/score" }, { name: "Users", icon: Users, path: "/admin/users" }, - { name: "View Leaderboard", icon: BarChart3, path: "/admin/leaderboard" } + { name: "View Leaderboard", icon: Trophy, path: "/admin/leaderboard" } ]; const handleMenuClick = (itemName: string, path: string) => { diff --git a/frontend/src/shared/layout/AppLayout.tsx b/frontend/src/shared/layout/AppLayout.tsx index fd69e585..b41c295f 100644 --- a/frontend/src/shared/layout/AppLayout.tsx +++ b/frontend/src/shared/layout/AppLayout.tsx @@ -7,7 +7,7 @@ interface AppLayoutProps { const AppLayout = ({ children }: AppLayoutProps) => { return (
-
+
{children}
From 942c23375a8bc3c2757d83ad40e6de9bdbb60f28 Mon Sep 17 00:00:00 2001 From: VishakhaSainani-Josh Date: Fri, 5 Sep 2025 12:42:44 +0530 Subject: [PATCH 31/36] fix goal summary --- backend/internal/repository/goal.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/backend/internal/repository/goal.go b/backend/internal/repository/goal.go index 0f3fa6da..80193856 100644 --- a/backend/internal/repository/goal.go +++ b/backend/internal/repository/goal.go @@ -120,9 +120,13 @@ const ( fetchUserGoalSummaryQuery = "SELECT * FROM goal_summary WHERE user_id=$1" - getUserGoalSummaryBySnapshotDateQuery = "SELECT * FROM goal_summary WHERE snapshot_date<$1 AND user_id=$2" + getUserGoalSummaryBySnapshotDateQuery = ` + SELECT * FROM goal_summary + WHERE user_id = $2 + AND snapshot_date::date = $1::date + LIMIT 1` - updateUserGoalSummaryQuery = "UPDATE user_goal SET snapshot_date=$2, incomplete_goals_count=$3, target_set=$4, target_completed=$5 where id=$1 " + updateUserGoalSummaryQuery = "UPDATE goal_summary SET snapshot_date=$2, incomplete_goals_count=$3, target_set=$4, target_completed=$5 where id=$1 " ) func (gr *goalRepository) ListGoalLevels(ctx context.Context, tx *sqlx.Tx) ([]GoalLevel, error) { From 0a95a4ca611fcbf4fe2d4e4dbb2be1d9f736b802 Mon Sep 17 00:00:00 2001 From: VishakhaSainani-Josh Date: Fri, 5 Sep 2025 12:52:11 +0530 Subject: [PATCH 32/36] add logo --- frontend/public/logo.svg | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 frontend/public/logo.svg diff --git a/frontend/public/logo.svg b/frontend/public/logo.svg new file mode 100644 index 00000000..894ae571 --- /dev/null +++ b/frontend/public/logo.svg @@ -0,0 +1,30 @@ + + Code Curiosity Logo + Minimal logo with coding brackets, a centered medal, and gradient colors to represent coding, open source contributions, and recognition. + + + + + + + + + + + + + + + + + + + + + + + + + + From 679b25c66bd47583c511ecb5218e07810476503c Mon Sep 17 00:00:00 2001 From: VishakhaSainani-Josh Date: Tue, 30 Sep 2025 16:44:50 +0530 Subject: [PATCH 33/36] implement: show targets for each goal level --- backend/internal/app/goal/domain.go | 14 +- backend/internal/app/goal/handler.go | 23 +++ backend/internal/app/goal/service.go | 31 +++- backend/internal/app/router.go | 1 + backend/internal/repository/goal.go | 20 ++- frontend/index.html | 2 +- frontend/package.json | 2 + frontend/public/logo-cc.svg | 16 ++ frontend/src/api/queries/UserGoals.ts | 19 ++ .../src/features/Admin/ScoreConfigure.tsx | 14 +- .../components/Contributors.tsx | 2 +- .../components/ContributorsCard.tsx | 13 +- .../components/RepositoryActivities.tsx | 25 +-- .../components/UserGoalSummary.tsx | 9 +- .../components/UserDashboard/UserGoals.tsx | 164 +++++++++++++++--- frontend/src/shared/constants/query-keys.ts | 1 + frontend/src/shared/types/types.ts | 9 + 17 files changed, 308 insertions(+), 57 deletions(-) create mode 100644 frontend/public/logo-cc.svg diff --git a/backend/internal/app/goal/domain.go b/backend/internal/app/goal/domain.go index 99c6adbe..7edee439 100644 --- a/backend/internal/app/goal/domain.go +++ b/backend/internal/app/goal/domain.go @@ -104,9 +104,11 @@ type GoalSummary struct { UpdatedAt time.Time `json:"updatedAt"` } -// type MonthlyGoalSummary struct { -// Day time.Time `json:"Day"` -// IncompleteGoalsCount int `json:"IncompleteGoalsCount"` -// TargetSet int `json:"TargetSet"` -// TargetCompleted int `json:"TargetCompleted"` -// } +type GoalLevelTarget struct { + Id int `json:"id"` + GoalLevelId int `json:"goalLevelId"` + ContributionType string `json:"contributionType"` + Target int `json:"target"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} diff --git a/backend/internal/app/goal/handler.go b/backend/internal/app/goal/handler.go index 9629503c..451863fd 100644 --- a/backend/internal/app/goal/handler.go +++ b/backend/internal/app/goal/handler.go @@ -16,6 +16,7 @@ type handler struct { type Handler interface { ListGoalLevels(w http.ResponseWriter, r *http.Request) + FetchGoalLevelTargetByGoalLevel(w http.ResponseWriter, r *http.Request) CreateUserGoalInProgress(w http.ResponseWriter, r *http.Request) ResetUserCurrentGoalStatus(w http.ResponseWriter, r *http.Request) GetUserCurrentGoalStatus(w http.ResponseWriter, r *http.Request) @@ -42,6 +43,28 @@ func (h *handler) ListGoalLevels(w http.ResponseWriter, r *http.Request) { response.WriteJson(w, http.StatusOK, "goal levels fetched successfully", gaols) } +func (h *handler) FetchGoalLevelTargetByGoalLevel(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + var goalLevel GoalLevel + err := json.NewDecoder(r.Body).Decode(&goalLevel) + if err != nil { + slog.Error(apperrors.ErrFailedMarshal.Error(), "error", err) + response.WriteJson(w, http.StatusBadRequest, apperrors.ErrInvalidRequestBody.Error(), nil) + return + } + + goalLevelTargets, err := h.goalService.FetchGoalLevelTargetByGoalLevel(ctx, goalLevel) + if err != nil { + slog.Error("error fetching goal level targets by goal level", "error", err) + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) + return + } + + response.WriteJson(w, http.StatusOK, "goal level targets fetched successfully", goalLevelTargets) +} + func (h *handler) CreateUserGoalInProgress(w http.ResponseWriter, r *http.Request) { ctx := r.Context() diff --git a/backend/internal/app/goal/service.go b/backend/internal/app/goal/service.go index af9a2fdc..0ed38612 100644 --- a/backend/internal/app/goal/service.go +++ b/backend/internal/app/goal/service.go @@ -19,6 +19,7 @@ type service struct { type Service interface { ListGoalLevels(ctx context.Context) ([]GoalLevel, error) + FetchGoalLevelTargetByGoalLevel(ctx context.Context, goalLevel GoalLevel) ([]GoalLevelTarget, error) CreateUserGoalInProgress(ctx context.Context, userSelecetdGoal CreateUserGoalRequest, userId int) (UserGoal, error) CreateCustomUserGoalTarget(ctx context.Context, userSelectedCustomGoals []CustomTargetRequest, createdUserGoal UserGoal) ([]UserGoalTarget, error) SyncUserGoalProgress(ctx context.Context, userGoalTargets []UserGoalTarget, monthStartedAt time.Time, userId int) ([]UserGoalProgress, error) @@ -54,6 +55,34 @@ func (s *service) ListGoalLevels(ctx context.Context) ([]GoalLevel, error) { return serviceGoals, nil } +func(s *service) FetchGoalLevelTargetByGoalLevel(ctx context.Context, goalLevel GoalLevel) ([]GoalLevelTarget, error) { + goalLevelTargets, err := s.goalRepository.FetchGoalLevelTargetByGoalLevel(ctx, nil, repository.GoalLevel(goalLevel)) + if err != nil { + slog.Error("error fetching goal level target by goal level", "error", err) + return nil, err + } + + serviceGoalLevelTargets := make([]GoalLevelTarget, len(goalLevelTargets)) + for i, g := range goalLevelTargets { + contributionType, err := s.contributionRepository.GetContributionTypeByContributionScoreId(ctx, nil, g.ContributionScoreId) + if err != nil { + slog.Error("error fetching contribution type by contribution score id", "error", err) + return nil, err + } + + serviceGoalLevelTargets[i] = GoalLevelTarget{ + Id: g.Id, + GoalLevelId: g.GoalLevelId, + ContributionType: contributionType, + Target: g.Target, + CreatedAt: g.CreatedAt, + UpdatedAt: g.UpdatedAt, + } + } + + return serviceGoalLevelTargets, nil +} + func (s *service) CreateUserGoalInProgress(ctx context.Context, userSelecetdGoal CreateUserGoalRequest, userId int) (UserGoal, error) { now := time.Now().UTC() monthStartedAt := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.UTC) @@ -99,7 +128,7 @@ func (s *service) CreateUserGoalInProgress(ctx context.Context, userSelecetdGoal //check if goal level is not custom if userSelecetdGoal.Level != GoalLevelCustom { - goalLevelTargets, err := s.goalRepository.FetchGoalLevelTargetByGoalLevel(ctx, nil, goalLevel) + goalLevelTargets, err := s.goalRepository.FetchGoalLevelTargetByGoalLevelId(ctx, nil, goalLevel) if err != nil { slog.Error("error fetching goal level target", "error", err) return UserGoal{}, err diff --git a/backend/internal/app/router.go b/backend/internal/app/router.go index 21ebd575..a2779a48 100644 --- a/backend/internal/app/router.go +++ b/backend/internal/app/router.go @@ -35,6 +35,7 @@ func NewRouter(deps Dependencies) http.Handler { router.HandleFunc("GET /api/v1/user/leaderboard", middleware.Authentication(middleware.AuthorizeUnblockedUser(deps.UserHandler.GetCurrentUserRank), deps.AppCfg)) router.HandleFunc("GET /api/v1/goal/level", middleware.Authentication(deps.GoalHandler.ListGoalLevels, deps.AppCfg)) + router.HandleFunc("POST /api/v1/goal/level/targets", middleware.Authentication(deps.GoalHandler.FetchGoalLevelTargetByGoalLevel, deps.AppCfg)) router.HandleFunc("POST /api/v1/user/goal/level", middleware.Authentication(deps.GoalHandler.CreateUserGoalInProgress, deps.AppCfg)) router.HandleFunc("POST /api/v1/user/goal/level/reset", middleware.Authentication(deps.GoalHandler.ResetUserCurrentGoalStatus, deps.AppCfg)) router.HandleFunc("GET /api/v1/user/goal/level", middleware.Authentication(deps.GoalHandler.GetUserCurrentGoalStatus, deps.AppCfg)) diff --git a/backend/internal/repository/goal.go b/backend/internal/repository/goal.go index 80193856..4ce53ae6 100644 --- a/backend/internal/repository/goal.go +++ b/backend/internal/repository/goal.go @@ -20,6 +20,7 @@ type GoalRepository interface { ListGoalLevels(ctx context.Context, tx *sqlx.Tx) ([]GoalLevel, error) GetGoalLevelByLevel(ctx context.Context, tx *sqlx.Tx, level string) (GoalLevel, error) CreateUserGoalInProgress(ctx context.Context, tx *sqlx.Tx, userGoal UserGoal) (UserGoal, error) + FetchGoalLevelTargetByGoalLevelId(ctx context.Context, tx *sqlx.Tx, goalLevel GoalLevel) ([]GoalLevelTarget, error) FetchGoalLevelTargetByGoalLevel(ctx context.Context, tx *sqlx.Tx, goalLevel GoalLevel) ([]GoalLevelTarget, error) CreateUserGoalTarget(ctx context.Context, tx *sqlx.Tx, userGoalTarget UserGoalTarget) (UserGoalTarget, error) CreateUserGoalProgress(ctx context.Context, tx *sqlx.Tx, userGoalProgress UserGoalProgress) (UserGoalProgress, error) @@ -58,7 +59,9 @@ const ( ($1, $2, $3, $4) RETURNING *` - fetchGoalLevelTargetByGoalLevelQuery = "SELECT * FROM goal_level_target WHERE goal_level_id=$1" + fetchGoalLevelTargetByGoalLevelIdQuery = "SELECT * FROM goal_level_target WHERE goal_level_id=$1" + + fetchGoalLevelTargetByGoalLevelQuery = "SELECT * FROM goal_level_target WHERE goal_level_id=(SELECT id FROM goal_level WHERE level=$1)" createUserGoalTargetQuery = ` INSERT INTO user_goal_target( @@ -173,11 +176,24 @@ func (gr *goalRepository) CreateUserGoalInProgress(ctx context.Context, tx *sqlx return createdUserGoal, nil } +func (gr *goalRepository) FetchGoalLevelTargetByGoalLevelId(ctx context.Context, tx *sqlx.Tx, goalLevel GoalLevel) ([]GoalLevelTarget, error) { + executer := gr.BaseRepository.initiateQueryExecuter(tx) + + var goalLevelTargets []GoalLevelTarget + err := executer.SelectContext(ctx, &goalLevelTargets, fetchGoalLevelTargetByGoalLevelIdQuery, goalLevel.Id) + if err != nil { + slog.Error("error fetching goal level target by goal level", "error", err) + return nil, apperrors.ErrFetchingGoalLevelTargets + } + + return goalLevelTargets, nil +} + func (gr *goalRepository) FetchGoalLevelTargetByGoalLevel(ctx context.Context, tx *sqlx.Tx, goalLevel GoalLevel) ([]GoalLevelTarget, error) { executer := gr.BaseRepository.initiateQueryExecuter(tx) var goalLevelTargets []GoalLevelTarget - err := executer.SelectContext(ctx, &goalLevelTargets, fetchGoalLevelTargetByGoalLevelQuery, goalLevel.Id) + err := executer.SelectContext(ctx, &goalLevelTargets, fetchGoalLevelTargetByGoalLevelQuery, goalLevel.Level) if err != nil { slog.Error("error fetching goal level target by goal level", "error", err) return nil, apperrors.ErrFetchingGoalLevelTargets diff --git a/frontend/index.html b/frontend/index.html index 7c5230bb..d9c2ce70 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -2,7 +2,7 @@ - + Code Curiosity diff --git a/frontend/package.json b/frontend/package.json index 2133d940..c746519e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -25,6 +25,7 @@ "@tailwindcss/vite": "^4.1.11", "@tanstack/react-query": "^5.83.0", "@tanstack/react-query-devtools": "^5.83.0", + "@tanstack/react-table": "^8.21.3", "axios": "^1.10.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -36,6 +37,7 @@ "react-dom": "^19.1.0", "react-hook-form": "^7.62.0", "react-router-dom": "^7.7.0", + "recharts": "^3.1.2", "sonner": "^2.0.6", "tailwind-merge": "^3.3.1", "tailwindcss": "^4.1.11" diff --git a/frontend/public/logo-cc.svg b/frontend/public/logo-cc.svg new file mode 100644 index 00000000..9ad22d63 --- /dev/null +++ b/frontend/public/logo-cc.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/frontend/src/api/queries/UserGoals.ts b/frontend/src/api/queries/UserGoals.ts index 78429758..70fd5fdc 100644 --- a/frontend/src/api/queries/UserGoals.ts +++ b/frontend/src/api/queries/UserGoals.ts @@ -4,6 +4,7 @@ import { BACKEND_URL } from "@/shared/constants/endpoints"; import { useMutation, useQuery } from "@tanstack/react-query"; import { CONTRIBUTION_TYPES_QUERY_KEY, + GOAL_LEVEL_TARGETS_QUERY_KEY, GOAL_LEVELS_QUERY_KEY, USER_ACTIVE_GOAL_LEVEL_QUERY_KEY, USER_GOAL_LEVEL_SUMMARY_QUERY_KEY @@ -11,6 +12,7 @@ import { import type { ContributionTypeDetail, GoalLevel, + GoalLevelTarget, GoalSummary, SetUserGoalLevelRequest, UserCurrentGoalStatus, @@ -34,6 +36,23 @@ export const useGoalLevels = () => { }); }; +const fetchGoalLevelTargets = async ( + goalLevel: GoalLevel +): Promise> => { + const response = await api.post<{ + message: string; + data: GoalLevelTarget[]; + }>(`${BACKEND_URL}/api/v1/goal/level/targets`, goalLevel); + + return response.data; +}; + +export const useGoalLevelTargets = () => { + return useMutation({ + mutationFn: (goalLevel: GoalLevel) => fetchGoalLevelTargets(goalLevel) + }); +}; + const fetchUserCurrentGoalStatus = async (): Promise< ApiResponse > => { diff --git a/frontend/src/features/Admin/ScoreConfigure.tsx b/frontend/src/features/Admin/ScoreConfigure.tsx index ccdc5e7e..fd4099ce 100644 --- a/frontend/src/features/Admin/ScoreConfigure.tsx +++ b/frontend/src/features/Admin/ScoreConfigure.tsx @@ -16,8 +16,8 @@ const ScoreConfigure = () => { const { data, isLoading, isError } = useFetchContributionTypes(); const { mutate: configureScore, isPending } = useConfigureContributionScore(); - // keep all scores in state (so empty can be tracked) const [scores, setScores] = useState>({}); + const [version, setVersion] = useState(0); // force re-render after save if (isLoading) return
Loading...
; if (isError || !data?.data) @@ -37,7 +37,6 @@ const ScoreConfigure = () => { }; const handleSaveAll = () => { - // validate: no empty or invalid scores const invalid = Object.entries(scores).some( ([, score]) => score === "" || isNaN(Number(score)) ); @@ -56,6 +55,8 @@ const ScoreConfigure = () => { configureScore(updates, { onSuccess: () => { toast.success("Contribution scores updated successfully."); + // trigger re-render to update sorted order + setVersion(prev => prev + 1); }, onError: () => { toast.error("Failed to update contribution scores."); @@ -64,6 +65,12 @@ const ScoreConfigure = () => { }); }; + // Sort data based on current scores + const sortedData = [...data.data].sort( + (a, b) => + Number(scores[a.contributionType]) - Number(scores[b.contributionType]) + ); + return (
@@ -71,8 +78,9 @@ const ScoreConfigure = () => { Configure Contribution Scores
+
- {data.data.map((item: ContributionScore) => ( + {sortedData.map((item: ContributionScore) => (
{ const contributorsData = viewAll ? contributors : contributors?.slice(0, 20); return ( -
+

Contributors {contributors.length} diff --git a/frontend/src/features/RepositoryDetails/components/ContributorsCard.tsx b/frontend/src/features/RepositoryDetails/components/ContributorsCard.tsx index f1fb3692..56428ac9 100644 --- a/frontend/src/features/RepositoryDetails/components/ContributorsCard.tsx +++ b/frontend/src/features/RepositoryDetails/components/ContributorsCard.tsx @@ -16,19 +16,20 @@ const ContributorsCard: FC = ({ githubUrl }) => { return ( -

+
- + Contributors-Image -
- {name}
- {contributions} Contributions -
+ + {/* Tooltip */} +
+

{name}

+

{contributions} Contributions

diff --git a/frontend/src/features/RepositoryDetails/components/RepositoryActivities.tsx b/frontend/src/features/RepositoryDetails/components/RepositoryActivities.tsx index ac6e981a..06d3e9f9 100644 --- a/frontend/src/features/RepositoryDetails/components/RepositoryActivities.tsx +++ b/frontend/src/features/RepositoryDetails/components/RepositoryActivities.tsx @@ -3,7 +3,7 @@ import clsx from "clsx"; import { Button } from "@/shared/components/ui/button"; import { Card } from "@/shared/components/ui/card"; import ActivityCard from "@/shared/components/common/ActivityCard"; -import { useParams } from "react-router-dom"; +import { useParams } from "react-router-dom"; import { TrendingUp } from "lucide-react"; import { useRepositoryActivities } from "@/api/queries/RepostoryActivities"; import CoinsInfo from "@/shared/components/common/CoinsInfo"; @@ -36,7 +36,7 @@ const RepositoryActivities: FC = ({ className }) => { ); } else if (repositoryActivitiesData?.length === 0) { content = ( -
+

No recent activities found @@ -47,25 +47,28 @@ const RepositoryActivities: FC = ({ className }) => { content = (

{repositoryActivitiesData?.map((activity, index) => ( ))} - {!viewAll && ( -
- -
- )} + {!viewAll && ( +
+ +
+ )}
); } @@ -73,7 +76,7 @@ const RepositoryActivities: FC = ({ className }) => { return ( diff --git a/frontend/src/features/UserDashboard/components/UserGoalSummary.tsx b/frontend/src/features/UserDashboard/components/UserGoalSummary.tsx index 6d9ce1be..2d668f39 100644 --- a/frontend/src/features/UserDashboard/components/UserGoalSummary.tsx +++ b/frontend/src/features/UserDashboard/components/UserGoalSummary.tsx @@ -14,7 +14,14 @@ const UserGoalSummaryChart = () => { const { data, isLoading, isError } = useUserGoalSummary(); if (isLoading) return
Loading...
; - if (isError || !data) return
Error loading goal summary
; + if (!data) + return ( +
+ Please set goals to view goal summary. Goal summary is update after 24 + hours. +
+ ); + if (isError) return
Error loading goal summary
; const summaryData = data.data; const chartData = summaryData.map(item => ({ diff --git a/frontend/src/shared/components/UserDashboard/UserGoals.tsx b/frontend/src/shared/components/UserDashboard/UserGoals.tsx index c2aa226f..a541d4ca 100644 --- a/frontend/src/shared/components/UserDashboard/UserGoals.tsx +++ b/frontend/src/shared/components/UserDashboard/UserGoals.tsx @@ -13,6 +13,7 @@ import { Loader2 } from "lucide-react"; import { useAllContributionTypes, useGoalLevels, + useGoalLevelTargets, useResetUserGoalStatus, useSetUserGoalLevel, useUserCurrentGoalStatus @@ -30,15 +31,19 @@ const UserGoals = () => { const [resetDialogOpen, setResetDialogOpen] = useState(false); const [isSettingLevel, setIsSettingLevel] = useState(false); const [isCustomDialogOpen, setIsCustomDialogOpen] = useState(false); + const [levelTargetDialogOpen, setLevelTargetDialogOpen] = useState(false); const [customGoals, setCustomGoals] = useState([]); const [selectedType, setSelectedType] = useState(""); const [target, setTarget] = useState(""); + const [selectedLevel, setSelectedLevel] = useState(""); const { data: userGoalLevelRes, isLoading: isGoalLevelLoading } = useUserCurrentGoalStatus(); const { data: goalLevelsRes, isLoading: isGoalLevelsLoading } = useGoalLevels(); + const { mutate: goalLevel, data: goalLevelTargetData } = + useGoalLevelTargets(); const { mutate: setGoalLevel } = useSetUserGoalLevel(); const { mutate: resetGoalStatus } = useResetUserGoalStatus(); const { data: contributionTypesRes } = useAllContributionTypes(); @@ -47,6 +52,7 @@ const UserGoals = () => { const userLevel = userGoalLevelRes?.data ?? null; const goalLevels = goalLevelsRes?.data ?? []; + const goalLevelTargets = goalLevelTargetData?.data ?? []; const allTypes: ContributionTypeDetail[] = contributionTypesRes?.data ?? []; const createdAt = userLevel?.createdAt @@ -136,6 +142,17 @@ const UserGoals = () => { ); }; + const handleViewLevelTarget = (selectedLevel: { + id: number; + level: string; + createdAt: string; + updatedAt: string; + }) => { + goalLevel(selectedLevel); + setSelectedLevel(selectedLevel.level); + setLevelTargetDialogOpen(true); + }; + if (isGoalLevelLoading || isGoalLevelsLoading) { return (
@@ -215,28 +232,59 @@ const UserGoals = () => { You haven't selected a goal level yet. Choose a level to start tracking contributions.

- - + + - - - + + + Select Goal Level + {!isSettingLevel ? ( -
- {goalLevels.map(level => ( - - ))} +
+
+ {goalLevels + .filter(level => level.level !== "Custom") + .map(level => ( +
+ + + +
+ ))} +
+
) : (
@@ -244,7 +292,8 @@ const UserGoals = () => { Setting your goal...
)} - + +
)} + {/* Level Target Dialog */} + + + + + Target for Level: {selectedLevel} + + + +
+

+ + Contribution Type + + + Target + +

+ + {goalLevelTargets.map(goal => ( +
+ + {goal.contributionType} + + + {goal.target} + +
+ ))} +
+ + + + +
+
+ {/* Custom Goal Dialog */} @@ -273,20 +371,36 @@ const UserGoals = () => { className="w-full rounded border p-2" > - {allTypes.map(type => ( - - ))} + {allTypes.map(type => { + const isAlreadyAdded = customGoals.some( + goal => goal.contributionType === type.contributionType + ); + return ( + + ); + })} setTarget(e.target.value)} + onChange={e => { + const val = e.target.value; + if (/^[1-9][0-9]*$/.test(val) || val === "") { + setTarget(val); + } + }} /> + {/* Custom Goal Dialog */} - + { + setIsCustomDialogOpen(open); + if (!open) { + setCustomGoals([]); + setSelectedType(""); + setTarget(""); + } + }} + > Set Custom Contribution Goals @@ -414,13 +449,16 @@ const UserGoals = () => { {customGoals.map((goal, idx) => (
- {goal.contributionType} - {goal.target} + + {goal.contributionType} + + {goal.target} From 086acac70cfbf0d59ce7efe7e8297dc360387c3d Mon Sep 17 00:00:00 2001 From: VishakhaSainani-Josh Date: Wed, 1 Oct 2025 15:36:49 +0530 Subject: [PATCH 36/36] fix graph summary --- backend/internal/app/contribution/service.go | 10 +++--- backend/internal/app/goal/service.go | 36 ++++++++++++-------- backend/internal/repository/goal.go | 17 +-------- 3 files changed, 27 insertions(+), 36 deletions(-) diff --git a/backend/internal/app/contribution/service.go b/backend/internal/app/contribution/service.go index 7428ef5b..69a6c2f1 100644 --- a/backend/internal/app/contribution/service.go +++ b/backend/internal/app/contribution/service.go @@ -125,16 +125,16 @@ func (s *service) ProcessFetchedContributions(ctx context.Context) error { } } - usersWithActiveGoalsForCurrentMonth, err := s.goalService.FetchUsersWithActiveGoalsForCurrentMonth(ctx) + users, err := s.userService.ListAllUsers(ctx) if err != nil { - slog.Error("error fetching users with active goals for current month", "error", err) + slog.Error("error fetching all users", "error", err) return err } - for _, userId := range usersWithActiveGoalsForCurrentMonth { - err := s.HandleGoalSynchronization(ctx, userId) + for _, user := range users { + err := s.HandleGoalSynchronization(ctx, user.Id) if err != nil { - slog.Error("error handling goal synchronization for user", "user id", userId, "error", err) + slog.Error("error handling goal synchronization for user", "user id", user.Id, "error", err) continue } } diff --git a/backend/internal/app/goal/service.go b/backend/internal/app/goal/service.go index 0d976ffb..f23ccaf3 100644 --- a/backend/internal/app/goal/service.go +++ b/backend/internal/app/goal/service.go @@ -30,7 +30,6 @@ type Service interface { SyncUserGoalProgressWithContributions(ctx context.Context, userId int) error CreateUserGoalSummary(ctx context.Context, userId int) (GoalSummary, error) FetchUserGoalSummary(ctx context.Context, userId int) ([]GoalSummary, error) - FetchUsersWithActiveGoalsForCurrentMonth(ctx context.Context) ([]int, error) } func NewService(goalRepository repository.GoalRepository, contributionRepository repository.ContributionRepository, badgeService badge.Service) Service { @@ -253,7 +252,7 @@ func (s *service) ResetUserCurrentGoalStatus(ctx context.Context, userId int) (U func (s *service) GetUserCurrentGoalStatus(ctx context.Context, userId int) (*GetUserCurrentGoalStatusResponse, error) { userCurrentGoal, err := s.goalRepository.GetUserCurrentGoal(ctx, nil, userId) if err != nil { - slog.Error("error getting user goal for current month") + slog.Error("error getting user goal for current month", "error", err) return nil, err } @@ -335,6 +334,11 @@ func (s *service) GetUserCurrentGoalStatus(ctx context.Context, userId int) (*Ge func (s *service) SyncUserGoalProgressWithContributions(ctx context.Context, userId int) error { userCurrentGoal, err := s.goalRepository.GetUserCurrentGoal(ctx, nil, userId) if err != nil { + if errors.Is(err, apperrors.ErrUserGoalNotFound) { + slog.Info("user does not have active goal for current month, skipping goal synchronization") + return nil + } + slog.Error("error getting user goal for current month", "error", err) return err } @@ -364,6 +368,11 @@ func (s *service) SyncUserGoalProgressWithContributions(ctx context.Context, use func (s *service) AllocateBadge(ctx context.Context, userId int) error { userCurrentGoalStatus, err := s.GetUserCurrentGoalStatus(ctx, userId) if err != nil { + if errors.Is(err, apperrors.ErrUserGoalNotFound) { + slog.Info("user does not have active goal for current month, skipping badge allocation") + return nil + } + slog.Error("error fetching user current goal status", "error", err) return err } @@ -422,14 +431,21 @@ func (s *service) UpdateUserGoalStatusMonthly(ctx context.Context) error { func (s *service) CreateUserGoalSummary(ctx context.Context, userId int) (GoalSummary, error) { userIncompleteGoalCount, err := s.goalRepository.CalculateUserIncompleteGoalsUntilDay(ctx, nil, userId) if err != nil { - slog.Error("error calculating user incomplete goalstatus until day", "error", err) + slog.Error("error calculating user incomplete goal status until day", "error", err) return GoalSummary{}, err } userCurrentGoalStatus, err := s.GetUserCurrentGoalStatus(ctx, userId) if err != nil { - slog.Error("error getting user current goal status", "error", err) - return GoalSummary{}, err + if errors.Is(err, apperrors.ErrUserGoalNotFound) { + slog.Info("user does not have active goal for current month") + userCurrentGoalStatus = &GetUserCurrentGoalStatusResponse{ + GoalTargetProgress: []UserGoalTargetProgress{}, + } + } else { + slog.Error("error getting user current goal status", "error", err) + return GoalSummary{}, err + } } var totalTargetSet int @@ -487,13 +503,3 @@ func (s *service) FetchUserGoalSummary(ctx context.Context, userId int) ([]GoalS return serviceUserGoalSummary, nil } - -func (s *service) FetchUsersWithActiveGoalsForCurrentMonth(ctx context.Context) ([]int, error) { - userIds, err := s.goalRepository.FetchUsersWithActiveGoalsForCurrentMonth(ctx, nil) - if err != nil { - slog.Error("error fetching users with active goals for current month", "error", err) - return nil, err - } - - return userIds, nil -} diff --git a/backend/internal/repository/goal.go b/backend/internal/repository/goal.go index 0a533640..00d8cb70 100644 --- a/backend/internal/repository/goal.go +++ b/backend/internal/repository/goal.go @@ -35,7 +35,6 @@ type GoalRepository interface { FetchUserGoalSummary(ctx context.Context, tx *sqlx.Tx, userId int) ([]GoalSummary, error) GetUserGoalSummaryBySnapshotDate(ctx context.Context, tx *sqlx.Tx, snapshotDate time.Time, userId int) (*GoalSummary, error) UpdateUserGoalSummary(ctx context.Context, tx *sqlx.Tx, goalSummaryId int, userGoalSummary GoalSummary) (GoalSummary, error) - FetchUsersWithActiveGoalsForCurrentMonth(ctx context.Context, tx *sqlx.Tx) ([]int, error) } func NewGoalRepository(db *sqlx.DB) GoalRepository { @@ -399,8 +398,7 @@ func (gr *goalRepository) GetUserGoalSummaryBySnapshotDate(ctx context.Context, func (gr *goalRepository) UpdateUserGoalSummary(ctx context.Context, tx *sqlx.Tx, goalSummaryId int, userGoalSummary GoalSummary) (GoalSummary, error) { executer := gr.BaseRepository.initiateQueryExecuter(tx) - var updatedUserGoalSummary GoalSummary - err := executer.GetContext(ctx, &updatedUserGoalSummary, updateUserGoalSummaryQuery, + _, err := executer.ExecContext(ctx, updateUserGoalSummaryQuery, goalSummaryId, userGoalSummary.SnapshotDate, userGoalSummary.IncompleteGoalsCount, @@ -414,16 +412,3 @@ func (gr *goalRepository) UpdateUserGoalSummary(ctx context.Context, tx *sqlx.Tx return GoalSummary{}, nil } - -func (gr *goalRepository) FetchUsersWithActiveGoalsForCurrentMonth(ctx context.Context, tx *sqlx.Tx) ([]int, error) { - executer := gr.BaseRepository.initiateQueryExecuter(tx) - - var userIds []int - err := executer.SelectContext(ctx, &userIds, fetchUsersWithActiveGoalsForCurrentMonthQuery) - if err != nil { - slog.Error("error fetching users with active goals for current month", "error", err) - return nil, apperrors.ErrInternalServer - } - - return userIds, nil -}