diff --git a/.gitignore b/.gitignore
index 9890de43..ab66fe80 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,28 @@
-local.yaml
\ No newline at end of file
+local.yaml
+.env
+
+# 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/backend/cmd/main.go b/backend/cmd/main.go
new file mode 100644
index 00000000..5ddb70ed
--- /dev/null
+++ b/backend/cmd/main.go
@@ -0,0 +1,152 @@
+package main
+
+import (
+ "context"
+ "fmt"
+ "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"
+ "github.com/joshsoftware/code-curiosity-2025/internal/db"
+ "github.com/rs/cors"
+ "github.com/urfave/cli"
+)
+
+func main() {
+ 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 err
+ }
+ defer db.Close()
+
+ bigqueryInstance, err := config.BigqueryInit(ctx, cfg)
+ if err != nil {
+ slog.Error("error initializing bigquery", "error", err)
+ return err
+ }
+
+ httpClient := &http.Client{}
+
+ dependencies := app.InitDependencies(db, cfg, bigqueryInstance, httpClient)
+
+ router := app.NewRouter(dependencies)
+
+ 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(
+ serverRunning,
+ syscall.SIGABRT,
+ syscall.SIGALRM,
+ syscall.SIGBUS,
+ syscall.SIGINT,
+ syscall.SIGTERM,
+ )
+
+ go func() {
+ slog.Info("server listening at", "port", cfg.HTTPServer.Port)
+
+ if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
+ slog.Error("server error", "error", err)
+ serverRunning <- syscall.SIGINT
+ }
+ }()
+
+ <-serverRunning
+
+ slog.Info("shutting down the server")
+ ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
+ defer cancel()
+
+ if err := server.Shutdown(ctx); err != nil {
+ slog.Error("cannot shut HTTP server down gracefully", "error", err)
+ }
+
+ slog.Info("server shutdown successfully")
+ return nil
+}
\ No newline at end of file
diff --git a/backend/go.mod b/backend/go.mod
new file mode 100644
index 00000000..6cdfd1ee
--- /dev/null
+++ b/backend/go.mod
@@ -0,0 +1,72 @@
+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/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.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
+ 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/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
+ 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/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..9f239732
--- /dev/null
+++ b/backend/go.sum
@@ -0,0 +1,233 @@
+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/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=
+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/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=
+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/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=
+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.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=
+olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3/go.mod h1:oVgVk4OWVDi43qWBEyGhXgYxt7+ED4iYNpTngSLX2Iw=
diff --git a/backend/internal/app/auth/domain.go b/backend/internal/app/auth/domain.go
new file mode 100644
index 00000000..c6c7575f
--- /dev/null
+++ b/backend/internal/app/auth/domain.go
@@ -0,0 +1,51 @@
+package auth
+
+import (
+ "database/sql"
+ "time"
+)
+
+const (
+ LoginWithGithubFailed = "LoginWithGithubFailed"
+ AccessTokenCookieName = "AccessToken"
+ GitHubOAuthState = "state"
+ GithubOauthScope = "read:user"
+ GetUserGithubUrl = "https://api.github.com/user"
+ GetUserEmailUrl = "https://api.github.com/user/emails"
+)
+
+type User struct {
+ Id int `json:"userId"`
+ GithubId int `json:"githubId"`
+ GithubUsername string `json:"githubUsername"`
+ Email string `json:"email"`
+ 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:"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"`
+ Email string `json:"email"`
+ IsAdmin bool `json:"is_admin"`
+ IsBlocked bool `json:"is_blocked"`
+}
+
+type AdminLoginRequest struct {
+ Email string `json:"email"`
+ Password string `json:"password"`
+}
+
+type Admin struct {
+ User
+ JwtToken string `json:"jwtToken"`
+}
diff --git a/internal/app/auth/handler.go b/backend/internal/app/auth/handler.go
similarity index 51%
rename from internal/app/auth/handler.go
rename to backend/internal/app/auth/handler.go
index 2be51640..249a0527 100644
--- a/internal/app/auth/handler.go
+++ b/backend/internal/app/auth/handler.go
@@ -1,30 +1,32 @@
package auth
import (
- "fmt"
+ "encoding/json"
"log/slog"
"net/http"
"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 {
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 {
return &handler{
authService: authService,
- appConfig: appConfig,
+ appConfig: appConfig,
}
}
@@ -44,25 +46,26 @@ 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) {
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)
@@ -72,3 +75,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/internal/app/auth/service.go b/backend/internal/app/auth/service.go
similarity index 65%
rename from internal/app/auth/service.go
rename to backend/internal/app/auth/service.go
index 3bca6c0d..a35a751a 100644
--- a/internal/app/auth/service.go
+++ b/backend/internal/app/auth/service.go
@@ -9,7 +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"
- "github.com/joshsoftware/code-curiosity-2025/internal/pkg/middleware"
+ "golang.org/x/crypto/bcrypt"
"golang.org/x/oauth2"
"golang.org/x/oauth2/github"
)
@@ -23,7 +23,8 @@ 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)
+ VerifyAdminCredentials(ctx context.Context, adminCredentials AdminLoginRequest) (Admin, error)
}
func NewService(userService user.Service, appCfg config.AppConfig) Service {
@@ -77,24 +78,24 @@ 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
}
+ 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
}
-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)
@@ -103,3 +104,30 @@ func (s *service) GetLoggedInUser(ctx context.Context) (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, adminInfo.IsBlocked, 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/badge/domain.go b/backend/internal/app/badge/domain.go
new file mode 100644
index 00000000..3811923a
--- /dev/null
+++ b/backend/internal/app/badge/domain.go
@@ -0,0 +1,12 @@
+package badge
+
+import "time"
+
+type Badge struct {
+ 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/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..70a08b33
--- /dev/null
+++ b/backend/internal/app/bigquery/domain.go
@@ -0,0 +1,45 @@
+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',
+ 'PushEvent'
+ )
+ 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..0fc11c51
--- /dev/null
+++ b/backend/internal/app/contribution/domain.go
@@ -0,0 +1,80 @@
+package contribution
+
+import "time"
+
+type ContributionResponse struct {
+ 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 `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 `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 `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 `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 `json:"type"`
+ Count int `json:"count"`
+ TotalCoins int `json:"totalCoins"`
+ Month time.Time `json:"month"`
+}
+
+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
new file mode 100644
index 00000000..dfac84c2
--- /dev/null
+++ b/backend/internal/app/contribution/handler.go
@@ -0,0 +1,138 @@
+package contribution
+
+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"
+ "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)
+ ListAllContributionTypes(w http.ResponseWriter, r *http.Request)
+ ConfigureContributionTypeScore(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()
+
+ 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)
+ 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)
+}
+
+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
new file mode 100644
index 00000000..69a6c2f1
--- /dev/null
+++ b/backend/internal/app/contribution/service.go
@@ -0,0 +1,408 @@
+package contribution
+
+import (
+ "context"
+ "encoding/json"
+ "log/slog"
+ "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"
+ "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"
+ issueCommentEvent = "IssueCommentEvent"
+ pullRequestCommentEvent = "PullRequestReviewCommentEvent"
+ pullRequestReviewEvent = "PullRequestReviewEvent"
+ pushEvent = "PushEvent"
+)
+
+// app contribution types
+const (
+ pullRequestMerged = "PullRequestMerged"
+ pullRequestOpened = "PullRequestOpened"
+ issueOpened = "IssueOpened"
+ issueClosed = "IssueClosed"
+ issueResolved = "IssueResolved"
+ issueComment = "IssueComment"
+ pullRequestComment = "PullRequestComment"
+ pullRequestReviewed = "PullRequestReviewed"
+ CommitAdded = "CommitAdded"
+)
+
+// payload
+const (
+ payloadActionKey = "action"
+ payloadPullRequestKey = "pull_request"
+ PayloadMergedKey = "merged"
+ PayloadIssueKey = "issue"
+ PayloadStateReasonKey = "state_reason"
+ PayloadClosedKey = "closed"
+ PayloadOpenedKey = "opened"
+ PayloadNotPlannedKey = "not_planned"
+ PayloadCompletedKey = "completed"
+ PayloadCreatedKey = "created"
+ PayloadApprovedKey = "approved"
+)
+
+type service struct {
+ bigqueryService bigquery.Service
+ contributionRepository repository.ContributionRepository
+ repositoryService repoService.Service
+ userService user.Service
+ transactionService transaction.Service
+ goalService goal.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, 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)
+ HandleGoalSynchronization(ctx context.Context, userId int) error
+}
+
+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,
+ }
+}
+
+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)
+ continue
+ }
+ }
+
+ users, err := s.userService.ListAllUsers(ctx)
+ if err != nil {
+ slog.Error("error fetching all users", "error", err)
+ return err
+ }
+
+ for _, user := range users {
+ err := s.HandleGoalSynchronization(ctx, user.Id)
+ if err != nil {
+ slog.Error("error handling goal synchronization for user", "user id", user.Id, "error", err)
+ continue
+ }
+ }
+
+ 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{})
+ 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{})
+ if stateReasonVal, ok := issue[PayloadStateReasonKey]; ok && stateReasonVal != nil {
+ if v, ok := stateReasonVal.(string); ok {
+ stateReason = v
+ }
+ }
+ }
+
+ 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 = CommitAdded
+
+ case pullRequestReviewEvent:
+ if action == PayloadCreatedKey || action == PayloadApprovedKey {
+ contributionType = pullRequestReviewed
+ }
+
+ case issueCommentEvent:
+ contributionType = issueComment
+
+ //if user.login not equal to contribution login
+ case pullRequestCommentEvent:
+ if action == PayloadCreatedKey {
+ 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 || contributionType == "" {
+ 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, 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
+ }
+
+ serviceContributions := make([]FetchUserContributionsResponse, len(userContributions))
+ for i, c := range userContributions {
+ 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
+}
+
+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
+}
+
+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
+}
+
+func (s *service) HandleGoalSynchronization(ctx context.Context, userId int) error {
+ err := s.goalService.SyncUserGoalProgressWithContributions(ctx, userId)
+ if err != nil {
+ slog.Error("error syncing goal progress with contibutions", "error", err)
+ return err
+ }
+
+ err = s.goalService.AllocateBadge(ctx, userId)
+ if err != nil {
+ slog.Error("error allocating badge", "error", err)
+ return err
+ }
+
+ _, err = s.goalService.CreateUserGoalSummary(ctx, userId)
+ if err != nil {
+ slog.Error("error creating goal summary for user", "error", err)
+ return err
+ }
+
+ return 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..7ab7f55f
--- /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 7 * * *", 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..a08e8de8
--- /dev/null
+++ b/backend/internal/app/cronJob/init.go
@@ -0,0 +1,36 @@
+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 {
+ //CHANGE AND SET TO UTC TIMEZONE
+ return &CronSchedular{
+ cron: cron.New(cron.WithLocation(time.UTC)),
+ }
+}
+
+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/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
new file mode 100644
index 00000000..7b7983b7
--- /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)
+
+ 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, repositoryService)
+ authService := auth.NewService(userService, appCfg)
+ bigqueryService := bigquery.NewService(client, userRepository)
+ transactionService := transaction.NewService(transactionRepository, userService)
+ contributionService := contribution.NewService(bigqueryService, contributionRepository, repositoryService, userService, transactionService, goalService, 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..94ab5438
--- /dev/null
+++ b/backend/internal/app/github/domain.go
@@ -0,0 +1,38 @@
+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 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:"avatar_url"`
+ GithubUrl string `json:"github_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..1ec62eff
--- /dev/null
+++ b/backend/internal/app/github/service.go
@@ -0,0 +1,98 @@
+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) ([]FetchRepositoryContributorsResponse, 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) ([]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 nil, err
+ }
+
+ var repoContributors []RepoContributorsResponse
+ err = json.Unmarshal(body, &repoContributors)
+ if err != nil {
+ slog.Error("error unmarshalling fetch contributors body", "error", err)
+ return nil, err
+ }
+
+ serviceRepoContributors := make([]FetchRepositoryContributorsResponse, len(repoContributors))
+ for i, c := range repoContributors {
+ serviceRepoContributors[i] = FetchRepositoryContributorsResponse(c)
+ }
+
+ return serviceRepoContributors, nil
+}
diff --git a/backend/internal/app/goal/domain.go b/backend/internal/app/goal/domain.go
new file mode 100644
index 00000000..7edee439
--- /dev/null
+++ b/backend/internal/app/goal/domain.go
@@ -0,0 +1,114 @@
+package goal
+
+import "time"
+
+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 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"`
+ 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 CreateUserGoalRequest struct {
+ Level string `json:"level"`
+ CustomTargets []CustomTargetRequest `json:"customTargets,omitempty"`
+}
+
+type CustomTargetRequest struct {
+ ContributionType string `json:"contributionType"`
+ Target int `json:"target"`
+}
+
+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"`
+ 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 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
new file mode 100644
index 00000000..451863fd
--- /dev/null
+++ b/backend/internal/app/goal/handler.go
@@ -0,0 +1,166 @@
+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)
+ 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)
+ FetchUserMonthlyGoalSummary(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 goal levels", "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) 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()
+
+ 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 userSelecetdGoal CreateUserGoalRequest
+ err := json.NewDecoder(r.Body).Decode(&userSelecetdGoal)
+ if err != nil {
+ 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, "Goal created successfully", userGoal)
+}
+
+func (h *handler) ResetUserCurrentGoalStatus(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
+ }
+
+ userResetGoalStatus, err := h.goalService.ResetUserCurrentGoalStatus(ctx, userId)
+ if err != nil {
+ slog.Error("error resetting user current goal status", "error", err)
+ status, errorMessage := apperrors.MapError(err)
+ response.WriteJson(w, status, errorMessage, nil)
+ return
+ }
+
+ 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("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, "user current goal status fetched successfully", userCurrentGoalStatus)
+}
+
+func (h *handler) FetchUserMonthlyGoalSummary(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
+ }
+
+ userMonthlyGoalSummary, err := h.goalService.FetchUserGoalSummary(ctx, userId)
+ if err != 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 monthly goal summary fetched successfully", userMonthlyGoalSummary)
+}
diff --git a/backend/internal/app/goal/service.go b/backend/internal/app/goal/service.go
new file mode 100644
index 00000000..f23ccaf3
--- /dev/null
+++ b/backend/internal/app/goal/service.go
@@ -0,0 +1,505 @@
+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"
+)
+
+type service struct {
+ goalRepository repository.GoalRepository
+ contributionRepository repository.ContributionRepository
+ badgeService badge.Service
+}
+
+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)
+ 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 {
+ return &service{
+ goalRepository: goalRepository,
+ contributionRepository: contributionRepository,
+ badgeService: badgeService,
+ }
+}
+
+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([]GoalLevel, len(goals))
+ for i, g := range goals {
+ serviceGoals[i] = GoalLevel(g)
+ }
+
+ 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)
+
+ userCurrentGoal, err := s.goalRepository.GetUserCurrentGoal(ctx, nil, userId)
+ if err == nil {
+ slog.Warn("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", "error", err)
+ return UserGoal{}, err
+ }
+
+ goalLevel, err := s.goalRepository.GetGoalLevelByLevel(ctx, nil, userSelecetdGoal.Level)
+ if err != nil {
+ slog.Error("error fetching goal id by goal level", "error", err)
+ return UserGoal{}, err
+ }
+
+ userGoal := UserGoal{
+ UserId: userId,
+ GoalLevelId: goalLevel.Id,
+ Status: GoalStatusInProgress,
+ MonthStartedAt: monthStartedAt,
+ }
+
+ createdUserGoal, err := s.goalRepository.CreateUserGoalInProgress(ctx, nil, repository.UserGoal(userGoal))
+ if err != nil {
+ slog.Error("failed to create user goal status", "error", err)
+ return UserGoal{}, err
+ }
+
+ 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.FetchGoalLevelTargetByGoalLevelId(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
+ }
+
+ createdUserGoalTargets = append(createdUserGoalTargets, UserGoalTarget(createdUserGoalTarget))
+ }
+ }
+
+ _, err = s.SyncUserGoalProgress(ctx, createdUserGoalTargets, monthStartedAt, userId)
+ if err != nil {
+ slog.Error("error syncing user goal progress", "error", err)
+ return UserGoal{}, err
+ }
+
+ return UserGoal(createdUserGoal), nil
+}
+
+func (s *service) CreateCustomUserGoalTarget(ctx context.Context, userSelectedCustomGoals []CustomTargetRequest, createdUserGoal UserGoal) ([]UserGoalTarget, error) {
+ createdUserGoalTargets := make([]UserGoalTarget, len(userSelectedCustomGoals))
+
+ for _, userSelectedCustomGoal := range userSelectedCustomGoals {
+ contributionScoreDetails, err := s.contributionRepository.GetContributionScoreDetailsByContributionType(ctx, nil, userSelectedCustomGoal.ContributionType)
+ if err != nil {
+ slog.Error("error getting contirbution score details for given contribution type", "error", err)
+ return nil, err
+ }
+
+ userGoalTarget := UserGoalTarget{
+ UserGoalId: createdUserGoal.Id,
+ ContributionScoreId: contributionScoreDetails.Id,
+ Target: userSelectedCustomGoal.Target,
+ }
+
+ createdUserGoalTarget, err := s.goalRepository.CreateUserGoalTarget(ctx, nil, repository.UserGoalTarget(userGoalTarget))
+ if err != nil {
+ slog.Error("error creeating user goal target", "error", err)
+ return nil, err
+ }
+
+ 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)
+ continue
+ }
+
+ createdUserGoalProgresses = append(createdUserGoalProgresses, UserGoalProgress(created))
+ }
+ }
+ }
+
+ 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
+ }
+
+ 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 getting user goal for current month", "error", err)
+ return nil, err
+ }
+
+ goalLevel, err := s.goalRepository.GetGoalLevelById(ctx, nil, userCurrentGoal.GoalLevelId)
+ if err != nil {
+ slog.Error("error fetching goal leve by goal level id", "error", err)
+ return nil, 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 nil, err
+ }
+
+ goalTargetProgresses := make([]UserGoalTargetProgress, 0, len(userCurrentGoalTargets))
+
+ var totalTargetsCompleted int
+ totalTargets := len(userCurrentGoalTargets)
+
+ for _, userCurrentGoalTarget := range userCurrentGoalTargets {
+
+ contributionType, err := s.contributionRepository.GetContributionTypeByContributionScoreId(ctx, nil, userCurrentGoalTarget.ContributionScoreId)
+ if err != nil {
+ slog.Error("error fetching contribution type by contribution score id", "error", err)
+ return nil, err
+ }
+
+ contributionProgressCount, err := s.goalRepository.GetContributionProgressCount(ctx, nil, userCurrentGoalTarget.Id)
+ if err != nil {
+ slog.Error("error fetching contribution progress count", "error", err)
+ return nil, err
+ }
+
+ goalTargetProgress := UserGoalTargetProgress{
+ ContributionType: contributionType,
+ Target: userCurrentGoalTarget.Target,
+ Progress: contributionProgressCount,
+ }
+
+ if goalTargetProgress.Target <= goalTargetProgress.Progress {
+ totalTargetsCompleted++
+ }
+
+ 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 {
+ 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
+ }
+
+ 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, len(userCurrentGoalTargets))
+ 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 {
+ 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
+ }
+
+ var totalTargetsCompleted int
+ totalTargets := len(userCurrentGoalStatus.GoalTargetProgress)
+ for _, goalTargetProgress := range userCurrentGoalStatus.GoalTargetProgress {
+ if goalTargetProgress.Target <= goalTargetProgress.Progress {
+ 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 goal status until day", "error", err)
+ return GoalSummary{}, err
+ }
+
+ 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")
+ userCurrentGoalStatus = &GetUserCurrentGoalStatusResponse{
+ GoalTargetProgress: []UserGoalTargetProgress{},
+ }
+ } else {
+ 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,
+ }
+
+ userGoalSummaryForToday, err := s.goalRepository.GetUserGoalSummaryBySnapshotDate(ctx, nil, userMonthlyGoalSummary.SnapshotDate, userId)
+ if err != nil {
+ slog.Error("error fetching user goal summary for today", "error", err)
+ return GoalSummary{}, err
+ }
+
+ 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) {
+ 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, len(usersGoalSummary))
+ for i, userGoalSummary := range usersGoalSummary {
+ serviceUserGoalSummary[i] = GoalSummary(userGoalSummary)
+ }
+
+ return serviceUserGoalSummary, nil
+}
diff --git a/backend/internal/app/repository/domain.go b/backend/internal/app/repository/domain.go
new file mode 100644
index 00000000..e9d86e30
--- /dev/null
+++ b/backend/internal/app/repository/domain.go
@@ -0,0 +1,61 @@
+package repository
+
+import "time"
+
+type Repository struct {
+ 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 `json:"languages"`
+ 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"`
+ 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 `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 `json:"name"`
+ Bytes int `json:"bytes"`
+ Percentage float64 `json:"percentage"`
+}
diff --git a/backend/internal/app/repository/handler.go b/backend/internal/app/repository/handler.go
new file mode 100644
index 00000000..58606146
--- /dev/null
+++ b/backend/internal/app/repository/handler.go
@@ -0,0 +1,181 @@
+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/middleware"
+ "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)
+ FetchParticularRepoContributors(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{}
+
+ 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)
+ 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.FetchParticularRepoDetails(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()
+
+ 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 {
+ 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, userId, 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..9f2afc31
--- /dev/null
+++ b/backend/internal/app/repository/service.go
@@ -0,0 +1,205 @@
+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, userId int) ([]FetchUsersContributedReposResponse, error)
+ FetchParticularRepoDetails(ctx context.Context, repoId int) (FetchParticularRepoDetailsResponse, 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)
+}
+
+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, 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
+ }
+
+ 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, userId, 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) 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, 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
+ }
+
+ 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
+}
+
+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/router.go b/backend/internal/app/router.go
new file mode 100644
index 00000000..a2779a48
--- /dev/null
+++ b/backend/internal/app/router.go
@@ -0,0 +1,52 @@
+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(middleware.AuthorizeUnblockedUser(deps.AuthHandler.GetLoggedInUser), 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(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(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(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("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))
+ 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))
+
+ 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))
+
+ 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..fa5a2055
--- /dev/null
+++ b/backend/internal/app/transaction/domain.go
@@ -0,0 +1,28 @@
+package transaction
+
+import "time"
+
+type Transaction struct {
+ 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 `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/transaction/service.go b/backend/internal/app/transaction/service.go
new file mode 100644
index 00000000..3fa01470
--- /dev/null
+++ b/backend/internal/app/transaction/service.go
@@ -0,0 +1,107 @@
+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) {
+ 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/backend/internal/app/user/domain.go b/backend/internal/app/user/domain.go
new file mode 100644
index 00000000..5e9a696f
--- /dev/null
+++ b/backend/internal/app/user/domain.go
@@ -0,0 +1,70 @@
+package user
+
+import (
+ "database/sql"
+ "time"
+)
+
+type User struct {
+ Id int `json:"userId"`
+ GithubId int `json:"githubId"`
+ GithubUsername string `json:"githubUsername"`
+ Email string `json:"email"`
+ 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:"isDeleted"`
+ DeletedAt sql.NullTime `json:"deletedAt"`
+ CreatedAt time.Time `json:"createdAt"`
+ UpdatedAt time.Time `json:"updatedAt"`
+}
+
+type CreateUserRequestBody struct {
+ GithubId int `json:"githubId"`
+ GithubUsername string `json:"githubUsername"`
+ AvatarUrl string `json:"avatarUrl"`
+ Email string `json:"email"`
+ IsAdmin bool `json:"isAdmin"`
+ IsBlocked bool `json:"isBlocked"`
+}
+
+type Email struct {
+ Email string `json:"email"`
+}
+
+type Transaction struct {
+ 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 `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 {
+ Level string `json:"level"`
+}
+
+type AdminLoginRequest struct {
+ 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
new file mode 100644
index 00000000..eda941cf
--- /dev/null
+++ b/backend/internal/app/user/handler.go
@@ -0,0 +1,212 @@
+package user
+
+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"
+ "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)
+ ListAllUsers(w http.ResponseWriter, r *http.Request)
+ BlockOrUnblockUser(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()
+
+ 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 {
+ slog.Error(apperrors.ErrFailedMarshal.Error(), "error", err)
+ response.WriteJson(w, http.StatusBadRequest, apperrors.ErrInvalidRequestBody.Error(), nil)
+ return
+ }
+
+ 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)
+ 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
+ }
+
+ 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)
+ 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)
+// }
+
+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
new file mode 100644
index 00000000..05fe19fe
--- /dev/null
+++ b/backend/internal/app/user/service.go
@@ -0,0 +1,241 @@
+package user
+
+import (
+ "context"
+ "log/slog"
+ "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
+ repositoryService repoService.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, userId int, 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)
+ 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 {
+ return &service{
+ userRepository: userRepository,
+ goalService: goalService,
+ repositoryService: repositoryService,
+ }
+}
+
+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, 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)
+ 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 {
+ 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
+}
+
+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
+ }
+
+ 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) {
+
+// 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
+// }
+
+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
+}
+
+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/internal/config/app.go b/backend/internal/config/app.go
similarity index 63%
rename from internal/config/app.go
rename to backend/internal/config/app.go
index c4715f5c..2f5f704b 100644
--- a/internal/config/app.go
+++ b/backend/internal/config/app.go
@@ -25,13 +25,20 @@ 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"`
+ AppName string `yaml:"app_name"`
+ 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..25081a60 100644
--- a/internal/db/migrate.go
+++ b/backend/internal/db/migrate.go
@@ -1,4 +1,4 @@
-package main
+package db
import (
"errors"
@@ -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
index 5419a1b9..fa375e10 100644
--- a/internal/db/migrations/1748862201_init.down.sql
+++ b/backend/internal/db/migrations/1748862201_init.down.sql
@@ -1,5 +1,4 @@
DROP TABLE IF EXISTS "goal_contribution";
-DROP TABLE IF EXISTS "goal";
DROP TABLE IF EXISTS "summary";
DROP TABLE IF EXISTS "badges";
DROP TABLE IF EXISTS "leaderboard_hourly";
@@ -8,3 +7,4 @@ DROP TABLE IF EXISTS "contributions";
DROP TABLE IF EXISTS "repositories";
DROP TABLE IF EXISTS "contribution_score";
DROP TABLE IF EXISTS "users";
+DROP TABLE IF EXISTS "goal";
diff --git a/internal/db/migrations/1748862201_init.up.sql b/backend/internal/db/migrations/1748862201_init.up.sql
similarity index 91%
rename from internal/db/migrations/1748862201_init.up.sql
rename to backend/internal/db/migrations/1748862201_init.up.sql
index 476693ac..da1ebc85 100644
--- a/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,
@@ -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..60222f22
--- /dev/null
+++ b/backend/internal/db/migrations/1752476063_create-index-users-current-balance.up.sql
@@ -0,0 +1,3 @@
+CREATE INDEX idx_users_current_balance
+ON users(current_balance DESC)
+WHERE is_admin = false AND is_deleted = false;
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
new file mode 100644
index 00000000..ce6fc047
--- /dev/null
+++ b/backend/internal/pkg/apperrors/errors.go
@@ -0,0 +1,90 @@
+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")
+ ErrUserBlocked = errors.New("blocked user")
+ 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")
+ 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")
+ 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")
+ 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 ")
+ 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")
+
+ ErrInvalidCredentials = errors.New("error invalid credentials")
+)
+
+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:
+ return http.StatusNotFound, err.Error()
+ case ErrInvalidToken:
+ return http.StatusUnprocessableEntity, err.Error()
+ default:
+ return http.StatusInternalServerError, err.Error()
+ }
+}
diff --git a/internal/pkg/jwt/jwt.go b/backend/internal/pkg/jwt/jwt.go
similarity index 86%
rename from internal/pkg/jwt/jwt.go
rename to backend/internal/pkg/jwt/jwt.go
index 1f5a936b..80a1a84f 100644
--- a/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/internal/pkg/middleware/middleware.go b/backend/internal/pkg/middleware/middleware.go
similarity index 53%
rename from internal/pkg/middleware/middleware.go
rename to backend/internal/pkg/middleware/middleware.go
index 3ece0897..9050c363 100644
--- a/internal/pkg/middleware/middleware.go
+++ b/backend/internal/pkg/middleware/middleware.go
@@ -5,23 +5,37 @@ 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 (
- 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 {
+ 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)
-
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
@@ -53,8 +67,46 @@ 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()
+ 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/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..e024eec3
--- /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.Set(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..b1353dbd
--- /dev/null
+++ b/backend/internal/repository/badge.go
@@ -0,0 +1,92 @@
+package repository
+
+import (
+ "context"
+ "database/sql"
+ "errors"
+ "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 {
+ 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
+ }
+
+ 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..81dfdf8c
--- /dev/null
+++ b/backend/internal/repository/contribution.go
@@ -0,0 +1,224 @@
+package repository
+
+import (
+ "context"
+ "database/sql"
+ "errors"
+ "log/slog"
+ "time"
+
+ "github.com/jmoiron/sqlx"
+ "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors"
+)
+
+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, 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)
+ 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 {
+ 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`
+
+ 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) {
+ 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, userId int) ([]Contribution, error) {
+ 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
+}
+
+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
+}
+
+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
new file mode 100644
index 00000000..3fbaa00f
--- /dev/null
+++ b/backend/internal/repository/domain.go
@@ -0,0 +1,178 @@
+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"`
+ IsBlocked bool `db:"is_blocked"`
+}
+
+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"`
+}
+
+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 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"`
+ UserGoalId int `db:"user_goal_id"`
+ ContributionScoreId int `db:"contribution_score_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"`
+ BadgeType string `db:"badge_type"`
+ EarnedAt time.Time `db:"earned_at"`
+ 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"`
+}
+
+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
new file mode 100644
index 00000000..00d8cb70
--- /dev/null
+++ b/backend/internal/repository/goal.go
@@ -0,0 +1,414 @@
+package repository
+
+import (
+ "context"
+ "database/sql"
+ "errors"
+ "log/slog"
+ "time"
+
+ "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) ([]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)
+ 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)
+ 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 {
+ return &goalRepository{
+ BaseRepository: BaseRepository{db},
+ }
+}
+
+const (
+ 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 *`
+
+ 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(
+ 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
+ ($1, $2, $3, $4, $5)
+ RETURNING *`
+
+ fetchUserGoalSummaryQuery = "SELECT * FROM goal_summary WHERE user_id=$1"
+
+ getUserGoalSummaryBySnapshotDateQuery = `
+ SELECT * FROM goal_summary
+ WHERE user_id = $2
+ AND snapshot_date::date = $1::date
+ LIMIT 1`
+
+ updateUserGoalSummaryQuery = "UPDATE goal_summary SET snapshot_date=$2, incomplete_goals_count=$3, target_set=$4, target_completed=$5 where id=$1 "
+
+ fetchUsersWithActiveGoalsForCurrentMonthQuery = `
+ SELECT DISTINCT(user_id) FROM user_goal
+ WHERE date_trunc('month', month_started_at) = date_trunc('month', NOW())`
+)
+
+func (gr *goalRepository) ListGoalLevels(ctx context.Context, tx *sqlx.Tx) ([]GoalLevel, error) {
+ executer := gr.BaseRepository.initiateQueryExecuter(tx)
+
+ 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 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) 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.Level)
+ 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) CreateUserGoalProgress(ctx context.Context, tx *sqlx.Tx, userGoalProgress UserGoalProgress) (UserGoalProgress, error) {
+ executer := gr.BaseRepository.initiateQueryExecuter(tx)
+
+ 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.Warn("no user goal found for current month", "userId", userId)
+ return UserGoal{}, apperrors.ErrUserGoalNotFound
+ }
+
+ 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 contributionProgressCount, nil
+}
+
+func (gr *goalRepository) GetGoalLevelById(ctx context.Context, tx *sqlx.Tx, goalLevelId int) (GoalLevel, error) {
+ executer := gr.BaseRepository.initiateQueryExecuter(tx)
+
+ var goalLevel GoalLevel
+ err := executer.GetContext(ctx, &goalLevel, getGoalLevelByIdQuery, goalLevelId)
+ 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 id", "error", err)
+ return GoalLevel{}, apperrors.ErrFailedToGetGoalLevel
+ }
+
+ 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) CreateUserGoalSummary(ctx context.Context, tx *sqlx.Tx, userGoalSummary GoalSummary) (GoalSummary, error) {
+ executer := gr.BaseRepository.initiateQueryExecuter(tx)
+
+ 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 user goal summary", "error", err)
+ return GoalSummary{}, err
+ }
+
+ return createdUserGoalSummary, nil
+}
+
+func (gr *goalRepository) FetchUserGoalSummary(ctx context.Context, tx *sqlx.Tx, userId int) ([]GoalSummary, error) {
+ executer := gr.BaseRepository.initiateQueryExecuter(tx)
+
+ var usersGoalSummary []GoalSummary
+ err := executer.SelectContext(ctx, &usersGoalSummary, fetchUserGoalSummaryQuery, userId)
+ if err != nil {
+ if errors.Is(err, sql.ErrNoRows) {
+ return nil, nil
+ }
+ slog.Error("error fetching users goal summary", "error", err)
+ return nil, err
+ }
+
+ 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)
+
+ _, err := executer.ExecContext(ctx, 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
+}
diff --git a/backend/internal/repository/repository.go b/backend/internal/repository/repository.go
new file mode 100644
index 00000000..4b2823c9
--- /dev/null
+++ b/backend/internal/repository/repository.go
@@ -0,0 +1,170 @@
+package repository
+
+import (
+ "context"
+ "database/sql"
+ "errors"
+ "log/slog"
+
+ "github.com/jmoiron/sqlx"
+ "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors"
+)
+
+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, 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)
+}
+
+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;`
+
+ 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) {
+ 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, 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)
+ return 0, apperrors.ErrCalculatingUserRepoTotalCoins
+ }
+
+ return totalCoins, nil
+}
+
+func (r *repositoryRepository) FetchUsersContributedRepos(ctx context.Context, tx *sqlx.Tx, userId int) ([]Repository, error) {
+ 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, userId int, repoGithubId int) ([]Contribution, error) {
+ 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
+}
+
+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/seed.go b/backend/internal/repository/seed.go
new file mode 100644
index 00000000..81cd7994
--- /dev/null
+++ b/backend/internal/repository/seed.go
@@ -0,0 +1,105 @@
+package repository
+
+import (
+ "fmt"
+ "log/slog"
+
+ "github.com/jmoiron/sqlx"
+ "github.com/joshsoftware/code-curiosity-2025/internal/config"
+ "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors"
+)
+
+func SeedData(appCfg config.AppConfig) error {
+ dbInfo := fmt.Sprintf(
+ "host=%s port=%d user=%s password=%s dbname=%s sslmode=disable",
+ appCfg.Database.Host,
+ appCfg.Database.Port,
+ appCfg.Database.User,
+ appCfg.Database.Password,
+ appCfg.Database.Name,
+ )
+
+ db, err := sqlx.Connect("postgres", dbInfo)
+ if err != nil {
+ return err
+ }
+
+ seedQueries := []string{
+ // Insert default admin user
+ `INSERT INTO users VALUES (
+ DEFAULT,
+ 0,
+ 'admin',
+ '',
+ '',
+ 0,
+ false,
+ true,
+ '$2a$14$gWxgkAc0uPTxkSBlMTudZusI/4QmQQssMXW8NjjZTJqsDx7PKdBvG',
+ false,
+ DEFAULT,
+ DEFAULT,
+ DEFAULT
+ )`,
+
+ // Contribution scores
+ `INSERT INTO contribution_score (admin_id, contribution_type, score)
+ VALUES
+ (1, 'CommitAdded', 10),
+ (1, 'IssueOpened', 10),
+ (1, 'IssueClosed', 20),
+ (1, 'IssueCompleted', 30),
+ (1, 'PullRequestOpened', 40),
+ (1, 'PullRequestMerged', 50),
+ (1, 'PullRequestUpdated', 60),
+ (1, 'IssueComment', 10),
+ (1, 'PullRequestComment', 10)`,
+
+ // Goal levels
+ `INSERT INTO goal_level (level)
+ VALUES
+ ('Custom'),
+ ('Beginner'),
+ ('Intermediate'),
+ ('Advanced')`,
+
+ // Goal level targets
+ `INSERT INTO goal_level_target (goal_level_id, contribution_score_id, target)
+ VALUES
+ (2, 5, 1),
+
+ (3, 1, 2),
+ (3, 2, 3),
+ (3, 3, 4),
+ (3, 4, 1),
+ (3, 5, 2),
+ (3, 6, 3),
+ (3, 7, 4),
+ (3, 8, 1),
+ (3, 9, 2),
+
+ (4, 1, 3),
+ (4, 2, 4),
+ (4, 3, 1),
+ (4, 4, 2),
+ (4, 5, 3),
+ (4, 6, 4),
+ (4, 7, 1),
+ (4, 8, 2),
+ (4, 9, 3)`,
+ }
+
+ for _, query := range seedQueries {
+ _, err := db.Exec(query)
+ if err != nil {
+ slog.Error("Failed to execute seed query",
+ "query", query,
+ "error", err,
+ )
+ return apperrors.ErrInternalServer
+ }
+ }
+
+ slog.Info("Seed data loaded successfully.")
+ return 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..6a1c4052
--- /dev/null
+++ b/backend/internal/repository/user.go
@@ -0,0 +1,300 @@
+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)
+ 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 {
+ 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, updated_at=$2 where id = $3"
+
+ 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"
+
+ getAllUsersGithubIdQuery = "SELECT github_id from users where is_admin=false"
+
+ 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
+ WHERE is_admin=false
+ AND is_deleted=false
+ 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
+ WHERE is_admin=false
+ AND is_deleted=false
+ )
+ ranked_users
+ WHERE id = $1;`
+
+ 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, 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.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, time.Now(), 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, time.Now(), 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) 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
+}
+
+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, time.Now(), userID)
+ if err != nil {
+ slog.Error("failed to update user block status", "error", err)
+ return apperrors.ErrInternalServer
+ }
+
+ return nil
+}
diff --git a/backend/run-backend.sh b/backend/run-backend.sh
new file mode 100755
index 00000000..b5175fd3
--- /dev/null
+++ b/backend/run-backend.sh
@@ -0,0 +1,4 @@
+export CONFIG_PATH=local.yaml
+
+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/cmd/main.go b/cmd/main.go
deleted file mode 100644
index 35ae97a7..00000000
--- a/cmd/main.go
+++ /dev/null
@@ -1,75 +0,0 @@
-package main
-
-import (
- "context"
- "fmt"
- "log/slog"
- "net/http"
- "os"
-
- "os/signal"
- "syscall"
- "time"
-
- "github.com/joshsoftware/code-curiosity-2025/internal/app"
- "github.com/joshsoftware/code-curiosity-2025/internal/config"
-)
-
-func main() {
- ctx := context.Background()
-
- 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)
- return
- }
- defer db.Close()
-
- dependencies := app.InitDependencies(db,cfg)
-
- router := app.NewRouter(dependencies)
-
- server := http.Server{
- Addr: fmt.Sprintf(":%s", cfg.HTTPServer.Port),
- Handler: router,
- }
-
- serverRunning := make(chan os.Signal, 1)
-
- signal.Notify(
- serverRunning,
- syscall.SIGABRT,
- syscall.SIGALRM,
- syscall.SIGBUS,
- syscall.SIGINT,
- syscall.SIGTERM,
- )
-
- go func() {
- slog.Info("server listening at", "port", cfg.HTTPServer.Port)
-
- if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
- slog.Error("server error", "error", err)
- serverRunning <- syscall.SIGINT
- }
- }()
-
- <-serverRunning
-
- slog.Info("shutting down the server")
- ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
- defer cancel()
-
- if err := server.Shutdown(ctx); err != nil {
- slog.Error("cannot shut HTTP server down gracefully", "error", err)
- }
-
- slog.Info("server shutdown successfully")
-}
diff --git a/frontend/.eslintrc.js b/frontend/.eslintrc.js
new file mode 100644
index 00000000..01301365
--- /dev/null
+++ b/frontend/.eslintrc.js
@@ -0,0 +1,56 @@
+import pluginJs from "@eslint/js";
+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"
+ }
+ }
+];
diff --git a/frontend/.prettierrc b/frontend/.prettierrc
new file mode 100644
index 00000000..3c92e9e0
--- /dev/null
+++ b/frontend/.prettierrc
@@ -0,0 +1,17 @@
+{
+ "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/components.json b/frontend/components.json
new file mode 100644
index 00000000..c2eba99e
--- /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": "@/shared/components",
+ "utils": "@/shared/utils/tailwindcss",
+ "ui": "@/shared/components/ui",
+ "lib": "@/shared/lib",
+ "hooks": "@/shared/hooks"
+ },
+ "iconLibrary": "lucide"
+}
diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js
new file mode 100644
index 00000000..c1b016ba
--- /dev/null
+++ b/frontend/eslint.config.js
@@ -0,0 +1,28 @@
+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"] },
+ {
+ 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..d9c2ce70
--- /dev/null
+++ b/frontend/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ Code Curiosity
+
+
+
+
+
+
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
new file mode 100644
index 00000000..ca01eccb
--- /dev/null
+++ b/frontend/package-lock.json
@@ -0,0 +1,6159 @@
+{
+ "name": "code-curiosity-frontend",
+ "version": "0.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "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",
+ "@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",
+ "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-hook-form": "^7.62.0",
+ "react-router-dom": "^7.7.0",
+ "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",
+ "@vitejs/plugin-react": "^4.7.0",
+ "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",
+ "prettier-plugin-tailwindcss": "^0.6.14",
+ "tw-animate-css": "^1.3.5",
+ "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==",
+ "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.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.28.2"
+ },
+ "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.2",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz",
+ "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==",
+ "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.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz",
+ "integrity": "sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "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"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "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"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "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"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.8.tgz",
+ "integrity": "sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "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"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.8.tgz",
+ "integrity": "sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "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"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "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"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "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"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "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"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "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"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "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"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "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"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "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"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "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"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "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"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.8.tgz",
+ "integrity": "sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "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"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "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"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "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"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "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"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.8.tgz",
+ "integrity": "sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "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"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "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"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "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"
+ ],
+ "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.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": {
+ "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.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": {
+ "@eslint/core": "^0.15.1",
+ "levn": "^0.4.1"
+ },
+ "engines": {
+ "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",
+ "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/@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==",
+ "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==",
+ "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==",
+ "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==",
+ "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.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"
+ },
+ "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-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",
+ "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-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": {
+ "@radix-ui/react-use-effect-event": "0.0.2",
+ "@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-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",
+ "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-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"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "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"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "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",
+ "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-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-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/rect": "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-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-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-visually-hidden": {
+ "version": "1.2.3",
+ "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-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/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",
+ "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"
+ ],
+ "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"
+ ],
+ "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"
+ ],
+ "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"
+ ],
+ "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"
+ ],
+ "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"
+ ],
+ "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"
+ ],
+ "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"
+ ],
+ "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"
+ ],
+ "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"
+ ],
+ "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"
+ ],
+ "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"
+ ],
+ "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"
+ ],
+ "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"
+ ],
+ "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"
+ ],
+ "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"
+ ],
+ "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"
+ ],
+ "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"
+ ],
+ "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"
+ ],
+ "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"
+ ],
+ "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",
+ "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/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",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "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/node": {
+ "version": "24.1.0",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz",
+ "integrity": "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==",
+ "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==",
+ "devOptional": 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==",
+ "devOptional": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^19.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "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.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",
+ "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.38.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.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.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": {
+ "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.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.38.0",
+ "@typescript-eslint/types": "^8.38.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.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.38.0",
+ "@typescript-eslint/visitor-keys": "8.38.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.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": {
+ "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.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.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"
+ },
+ "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.38.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.38.0.tgz",
+ "integrity": "sha512-wzkUfX3plUqij4YwWaJyqhiPE5UCRVlFpKn1oCRn2O1bJ592XxWJj8ROQ3JD5MYXLORW84063z3tZTb/cs4Tyw==",
+ "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.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.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",
+ "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.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.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"
+ },
+ "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.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.38.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/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",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "license": "MIT"
+ },
+ "node_modules/axios": {
+ "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.4",
+ "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/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",
+ "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==",
+ "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",
+ "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/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/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",
+ "integrity": "sha512-kQhDYKZecqnM0fCnzI5eIv5L4cAe/iRI+HqMbO/hbRdTAeXDG+M9FjipUxNfbARuEg4iHIbhnhs78BCHNbSxEQ==",
+ "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",
+ "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.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"
+ },
+ "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/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",
+ "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.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": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@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": {
+ "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.32.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.32.0.tgz",
+ "integrity": "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==",
+ "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.32.0",
+ "@eslint/plugin-kit": "^0.3.4",
+ "@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.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": {
+ "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.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": {
+ "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==",
+ "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-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",
+ "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/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",
+ "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/jiti": {
+ "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"
+ }
+ },
+ "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/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",
+ "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/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",
+ "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/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",
+ "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==",
+ "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/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",
+ "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==",
+ "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==",
+ "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/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",
+ "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-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",
+ "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "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",
+ "integrity": "sha512-jVKHXoWRIsD/qS6lvGveckwb862EekvapdHJN/cGmzw40KnJH5gg53ujOJ4qX6EKIK9LSBfFed/xiQ5yeXNrUA==",
+ "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.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.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "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",
+ "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==",
+ "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/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",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "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.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.9"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "funding": {
+ "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==",
+ "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==",
+ "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==",
+ "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/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",
+ "integrity": "sha512-9dy0R9UsYEGmgf26L8UcHiLmSFTHa9+D7+dAt/G/sF5dCnPePZbfgDYinc7/UzAM7g/baVrmS6m9yEpU46d+LA==",
+ "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",
+ "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.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.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"
+ },
+ "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/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",
+ "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/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",
+ "integrity": "sha512-MHFiOENNBd+Bd9uvc8GEsIzdkn1JxMmEeYX35tI3fv0sJBUTfW5tQsoaOwuY4KhBI09A3dUJ/DXf2yxPVPUceg==",
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.25.0",
+ "fdir": "^6.4.6",
+ "picomatch": "^4.0.3",
+ "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==",
+ "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==",
+ "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==",
+ "devOptional": 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..c746519e
--- /dev/null
+++ b/frontend/package.json
@@ -0,0 +1,74 @@
+{
+ "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": {
+ "@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",
+ "@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",
+ "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-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"
+ },
+ "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",
+ "@vitejs/plugin-react": "^4.7.0",
+ "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",
+ "prettier-plugin-tailwindcss": "^0.6.14",
+ "tw-animate-css": "^1.3.5",
+ "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/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/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 @@
+
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/api/axios.ts b/frontend/src/api/axios.ts
new file mode 100644
index 00000000..418d7f3d
--- /dev/null
+++ b/frontend/src/api/axios.ts
@@ -0,0 +1,33 @@
+import axios from "axios";
+
+import { BACKEND_URL } from "@/shared/constants/endpoints";
+import { clearAccessToken, getAccessToken } from "@/shared/utils/local-storage";
+import { LOGIN_PATH } from "@/shared/constants/routes";
+
+export const api = axios.create({
+ baseURL: BACKEND_URL
+});
+
+api.interceptors.request.use(
+ config => {
+ const token = getAccessToken();
+ if (token) {
+ config.headers.Authorization = `Bearer ${token}`;
+ }
+ return config;
+ },
+ 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
new file mode 100644
index 00000000..ed6e2dd6
--- /dev/null
+++ b/frontend/src/api/queries/Admin.ts
@@ -0,0 +1,108 @@
+import { BACKEND_URL } from "@/shared/constants/endpoints";
+import type { ApiResponse } from "@/shared/types/api";
+import { api } from "../axios";
+import { useMutation, useQuery } from "@tanstack/react-query";
+import type {
+ Admin,
+ AdminCredentials,
+ 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
+): Promise> => {
+ const response = await api.post<{
+ message: string;
+ data: Admin;
+ }>(`${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)
+ });
+};
+
+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: [GET_ALL_USERS_QUERY_KEY],
+ 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: [CONTRIBUTION_TYPES_QUERY_KEY],
+ 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..3cbc3797
--- /dev/null
+++ b/frontend/src/api/queries/Auth.ts
@@ -0,0 +1,23 @@
+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,
+ retry: false,
+ });
+};
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/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/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
new file mode 100644
index 00000000..34271e22
--- /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
+ });
+};
diff --git a/frontend/src/api/queries/UserGoals.ts b/frontend/src/api/queries/UserGoals.ts
new file mode 100644
index 00000000..70fd5fdc
--- /dev/null
+++ b/frontend/src/api/queries/UserGoals.ts
@@ -0,0 +1,139 @@
+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_LEVEL_TARGETS_QUERY_KEY,
+ GOAL_LEVELS_QUERY_KEY,
+ USER_ACTIVE_GOAL_LEVEL_QUERY_KEY,
+ USER_GOAL_LEVEL_SUMMARY_QUERY_KEY
+} from "@/shared/constants/query-keys";
+import type {
+ ContributionTypeDetail,
+ GoalLevel,
+ GoalLevelTarget,
+ GoalSummary,
+ SetUserGoalLevelRequest,
+ UserCurrentGoalStatus,
+ UserGoal,
+ UserGoalLevelStatus
+} from "@/shared/types/types";
+
+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 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
+> => {
+ const response = await api.get<{
+ message: string;
+ data: UserCurrentGoalStatus;
+ }>(`${BACKEND_URL}/api/v1/user/goal/level`);
+
+ return response.data;
+};
+
+export const useUserCurrentGoalStatus = () => {
+ return useQuery({
+ queryKey: [USER_ACTIVE_GOAL_LEVEL_QUERY_KEY],
+ queryFn: fetchUserCurrentGoalStatus
+ });
+};
+
+const setUserGoalLevel = async (
+ userGoalLevelRequest: SetUserGoalLevelRequest
+): Promise> => {
+ const response = await api.post<{
+ message: string;
+ data: UserGoalLevelStatus;
+ }>(`${BACKEND_URL}/api/v1/user/goal/level`, userGoalLevelRequest);
+
+ return response.data;
+};
+
+export const useSetUserGoalLevel = () => {
+ return useMutation({
+ 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
+> => {
+ 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
+ });
+};
+
+const fetchUserGoalSummary = async (): Promise> => {
+ const response = await api.get<{
+ message: string;
+ data: GoalSummary[];
+ }>(`${BACKEND_URL}/api/v1/user/goal/summary`);
+
+ return response.data;
+};
+
+export const useUserGoalSummary = () => {
+ return useQuery({
+ queryKey: [USER_GOAL_LEVEL_SUMMARY_QUERY_KEY],
+ queryFn: fetchUserGoalSummary
+ });
+};
diff --git a/frontend/src/api/queries/UserProfileDetails.ts b/frontend/src/api/queries/UserProfileDetails.ts
new file mode 100644
index 00000000..fba68ec3
--- /dev/null
+++ b/frontend/src/api/queries/UserProfileDetails.ts
@@ -0,0 +1,55 @@
+import type { User } from "@/shared/types/types";
+import { api } from "../axios";
+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`);
+
+ return response.data;
+};
+
+export const useLoggedInUser = (enabled: boolean = true) => {
+ return useQuery({
+ queryKey: [LOGGED_IN_USER_QUERY_KEY],
+ queryFn: fetchLoggedInUser,
+ enabled,
+ retry: false
+ });
+};
+
+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/api/react-query.ts b/frontend/src/api/react-query.ts
new file mode 100644
index 00000000..29c17f06
--- /dev/null
+++ b/frontend/src/api/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/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/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/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/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/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/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/information.png b/frontend/src/assets/information.png
new file mode 100644
index 00000000..66ed823c
Binary files /dev/null and b/frontend/src/assets/information.png differ
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/Admin/AdminLeaderboard.tsx b/frontend/src/features/Admin/AdminLeaderboard.tsx
new file mode 100644
index 00000000..54fc93b4
--- /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
new file mode 100644
index 00000000..a99d4d0b
--- /dev/null
+++ b/frontend/src/features/Admin/AdminLogin.tsx
@@ -0,0 +1,101 @@
+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,
+ 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";
+
+const AdminLogin: FC = () => {
+ const { register, handleSubmit } = useForm();
+ const navigate = useNavigate();
+ const { mutate, isPending } = useLogInAdmin();
+ const [showPassword, setShowPassword] = useState(false);
+
+ 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);
+ localStorage.setItem(USER_DATA_KEY, JSON.stringify(res.data));
+ navigate("/admin/users");
+ },
+ onError: err => {
+ toast.error("Invalid credentials");
+ console.error("Admin login error", err);
+ }
+ });
+ } catch (err) {
+ console.error("Unexpected submit error", err);
+ }
+ };
+
+ return (
+
+ );
+};
+
+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..fd4099ce
--- /dev/null
+++ b/frontend/src/features/Admin/ScoreConfigure.tsx
@@ -0,0 +1,115 @@
+import { useState } from "react";
+
+import { Button } from "@/shared/components/ui/button";
+import { Input } from "@/shared/components/ui/input";
+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>({});
+ const [version, setVersion] = useState(0); // force re-render after save
+
+ if (isLoading) return Loading...
;
+ if (isError || !data?.data)
+ return Failed to load contribution types.
;
+
+ // Initialize state with current scores (so they are controlled)
+ if (Object.keys(scores).length === 0) {
+ const init: Record = {};
+ data.data.forEach((item: ContributionScore) => {
+ init[item.contributionType] = String(item.score);
+ });
+ setScores(init);
+ }
+
+ const handleScoreChange = (contributionType: string, newScore: string) => {
+ setScores(prev => ({ ...prev, [contributionType]: newScore }));
+ };
+
+ const handleSaveAll = () => {
+ const invalid = Object.entries(scores).some(
+ ([, score]) => score === "" || isNaN(Number(score))
+ );
+ if (invalid) {
+ toast.error("All scores must be valid numbers (no empty fields).");
+ return;
+ }
+
+ const updates: ContributionScoreUpdate[] = Object.entries(scores).map(
+ ([contributionType, score]) => ({
+ contributionType,
+ score: Number(score)
+ })
+ );
+
+ 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.");
+ console.log(updates);
+ }
+ });
+ };
+
+ // Sort data based on current scores
+ const sortedData = [...data.data].sort(
+ (a, b) =>
+ Number(scores[a.contributionType]) - Number(scores[b.contributionType])
+ );
+
+ return (
+
+
+
+ Configure Contribution Scores
+
+
+
+
+ {sortedData.map((item: ContributionScore) => (
+
+
+ {item.contributionType.replace(/([a-z])([A-Z])/g, "$1 $2")}
+
+
+
+ handleScoreChange(item.contributionType, 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..9725e630
--- /dev/null
+++ b/frontend/src/features/Admin/Users.tsx
@@ -0,0 +1,394 @@
+import { type FC, useEffect, useState } from "react";
+import {
+ type ColumnDef,
+ flexRender,
+ getCoreRowModel,
+ getFilteredRowModel,
+ getPaginationRowModel,
+ useReactTable
+} from "@tanstack/react-table";
+import defaultAvatar from "@/assets/default-profile-pic.svg";
+import {
+ Loader,
+ User,
+ Shield,
+ ShieldOff,
+ ChevronLeft,
+ 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";
+
+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 [users, setUsers] = useState(data?.data || []);
+
+ useEffect(() => {
+ if (data?.data) {
+ setUsers(data.data);
+ }
+ }, [data]);
+
+ 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 columns: ColumnDef[] = [
+ {
+ accessorKey: "githubUsername",
+ header: "User",
+ cell: ({ row }) => {
+ const user = row.original;
+ return (
+
+

+
+ {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 (
+
+
+ Loading users...
+
+ );
+ }
+
+ if (!users.length) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
+ User Management
+
+
+
+ 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)`}
+
+
+
+
+
+
+
+ table.setGlobalFilter(e.target.value)}
+ className="pl-8"
+ />
+ {table.getState().globalFilter && (
+
+ )}
+
+
+
+
+
+
+
+
+ {table.getHeaderGroups().map(headerGroup => (
+
+ {headerGroup.headers.map(header => {
+ return (
+
+ {header.isPlaceholder
+ ? null
+ : flexRender(
+ header.column.columnDef.header,
+ header.getContext()
+ )}
+
+ );
+ })}
+
+ ))}
+
+
+ {table.getRowModel().rows?.length ? (
+ table.getRowModel().rows.map(row => (
+
+ {row.getVisibleCells().map(cell => (
+
+ {flexRender(
+ cell.column.columnDef.cell,
+ cell.getContext()
+ )}
+
+ ))}
+
+ ))
+ ) : (
+
+
+ No results.
+
+
+ )}
+
+
+
+
+ {/* Pagination */}
+
+
+
Rows per page
+
+
+
+
+
+ Page {table.getState().pagination.pageIndex + 1} of{" "}
+ {table.getPageCount()}
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/frontend/src/features/Login/components/LoginComponent.tsx b/frontend/src/features/Login/components/LoginComponent.tsx
new file mode 100644
index 00000000..4d26387a
--- /dev/null
+++ b/frontend/src/features/Login/components/LoginComponent.tsx
@@ -0,0 +1,78 @@
+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 { 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";
+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);
+ console.log("data", data);
+ console.log("isSuccess", isSuccess);
+ useEffect(() => {
+ if (!code) return;
+
+ if (isSuccess && data?.data) {
+ const token = data.data;
+ setAccessToken(token);
+
+ console.log("hello");
+ navigate("/");
+ }
+
+ if (isError) {
+ toast.error("OAuth login failed:");
+ }
+ }, [isSuccess, isError, data, navigate]);
+
+ const handleGithubLogin = () => {
+ window.location.href = GITHUB_AUTH_URL || "";
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ 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..e254c720
--- /dev/null
+++ b/frontend/src/features/Login/index.tsx
@@ -0,0 +1,7 @@
+import LoginComponent from "@/features/Login/components/LoginComponent";
+
+const Login = () => {
+ 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..fbfc5aec
--- /dev/null
+++ b/frontend/src/features/MyContributions/components/Repositories.tsx
@@ -0,0 +1,111 @@
+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 [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 (
+
+
+
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 ? (
+
+
+ No repositories found
+
+
+ Try adjusting your search or filters
+
+
+ ) : (
+ filteredRepos.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 (
+
+ );
+ })}
+
+
+
+ {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
new file mode 100644
index 00000000..38bce587
--- /dev/null
+++ b/frontend/src/features/MyContributions/index.tsx
@@ -0,0 +1,13 @@
+import Repositories from "./components/Repositories";
+
+const MyContributions = () => {
+ return (
+
+ );
+};
+
+export default MyContributions;
diff --git a/frontend/src/features/RepositoryDetails/components/Contributors.tsx b/frontend/src/features/RepositoryDetails/components/Contributors.tsx
new file mode 100644
index 00000000..4f2325cf
--- /dev/null
+++ b/frontend/src/features/RepositoryDetails/components/Contributors.tsx
@@ -0,0 +1,64 @@
+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/components/ContributorsCard.tsx b/frontend/src/features/RepositoryDetails/components/ContributorsCard.tsx
new file mode 100644
index 00000000..56428ac9
--- /dev/null
+++ b/frontend/src/features/RepositoryDetails/components/ContributorsCard.tsx
@@ -0,0 +1,40 @@
+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
+
+ {/* Tooltip */}
+
+
{name}
+
{contributions} Contributions
+
+
+
+
+ );
+};
+
+export default ContributorsCard;
diff --git a/frontend/src/features/RepositoryDetails/components/Languages.tsx b/frontend/src/features/RepositoryDetails/components/Languages.tsx
new file mode 100644
index 00000000..d47572f6
--- /dev/null
+++ b/frontend/src/features/RepositoryDetails/components/Languages.tsx
@@ -0,0 +1,26 @@
+import { type FC } from "react";
+import { useRepositoryLanguages } from "@/api/queries/Languages";
+import { useParams } from "react-router-dom";
+import LanguageCard from "./LanguagesCard";
+
+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/components/LanguagesCard.tsx b/frontend/src/features/RepositoryDetails/components/LanguagesCard.tsx
new file mode 100644
index 00000000..15d0fde2
--- /dev/null
+++ b/frontend/src/features/RepositoryDetails/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/components/Repository.tsx b/frontend/src/features/RepositoryDetails/components/Repository.tsx
new file mode 100644
index 00000000..fc5f4cb6
--- /dev/null
+++ b/frontend/src/features/RepositoryDetails/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/components/RepositoryActivities.tsx b/frontend/src/features/RepositoryDetails/components/RepositoryActivities.tsx
new file mode 100644
index 00000000..06d3e9f9
--- /dev/null
+++ b/frontend/src/features/RepositoryDetails/components/RepositoryActivities.tsx
@@ -0,0 +1,98 @@
+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 { useParams } from "react-router-dom";
+import { TrendingUp } from "lucide-react";
+import { useRepositoryActivities } from "@/api/queries/RepostoryActivities";
+import CoinsInfo from "@/shared/components/common/CoinsInfo";
+
+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);
+
+ let content;
+ if (isLoading) {
+ content = (
+
+ );
+ } else if (repositoryActivitiesData?.length === 0) {
+ content = (
+
+
+
+ No recent activities found
+
+
+ );
+ } else {
+ content = (
+
+ {repositoryActivitiesData?.map((activity, index) => (
+
+ ))}
+ {!viewAll && (
+
+
+
+ )}
+
+ );
+ }
+
+ return (
+
+
+
Recent Activities
+
+
+ {content}
+
+ );
+};
+
+export default RepositoryActivities;
diff --git a/frontend/src/features/RepositoryDetails/components/RepositoryCard.tsx b/frontend/src/features/RepositoryDetails/components/RepositoryCard.tsx
new file mode 100644
index 00000000..04945440
--- /dev/null
+++ b/frontend/src/features/RepositoryDetails/components/RepositoryCard.tsx
@@ -0,0 +1,65 @@
+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 (
+
+
+
+
+ {languages?.map((language, index) => {
+ const color = LangColor[language] || "bg-gray-400";
+ return (
+
+ );
+ })}
+
+
+
+ {description ||
+ "No description for the given repository. Lorem ipsum dolor sit amet."}
+
+
+ );
+};
+
+export default RepositoryCard;
diff --git a/frontend/src/features/RepositoryDetails/index.tsx b/frontend/src/features/RepositoryDetails/index.tsx
new file mode 100644
index 00000000..85dcfce5
--- /dev/null
+++ b/frontend/src/features/RepositoryDetails/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/Leaderboard.tsx b/frontend/src/features/UserDashboard/components/Leaderboard.tsx
new file mode 100644
index 00000000..86900d64
--- /dev/null
+++ b/frontend/src/features/UserDashboard/components/Leaderboard.tsx
@@ -0,0 +1,94 @@
+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";
+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 = () => {
+ 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 && !isAdmin && (
+
+
+
+ )}
+ >
+ )}
+
+ );
+};
+
+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..e5fb648f
--- /dev/null
+++ b/frontend/src/features/UserDashboard/components/Overview.tsx
@@ -0,0 +1,113 @@
+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 for the selected period.
+
+
+ ) : (
+
+ {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..a0652298
--- /dev/null
+++ b/frontend/src/features/UserDashboard/components/OverviewCard.tsx
@@ -0,0 +1,34 @@
+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.replace(/([A-Z])/g, " $1")}
+
+
+
+ {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..ab0366e7
--- /dev/null
+++ b/frontend/src/features/UserDashboard/components/RecentActivities.tsx
@@ -0,0 +1,85 @@
+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 { TrendingUp } from "lucide-react";
+import CoinsInfo from "@/shared/components/common/CoinsInfo";
+
+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 && (
+
+
+
+ )}
+
+ )}
+
+ );
+};
+
+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..8943e036
--- /dev/null
+++ b/frontend/src/features/UserDashboard/components/UserDashboardComponent.tsx
@@ -0,0 +1,23 @@
+import Leaderboard from "@/features/UserDashboard/components/Leaderboard";
+import RecentActivities from "@/features/UserDashboard/components/RecentActivities";
+import Overview from "@/features/UserDashboard/components/Overview";
+import UserGoalSummaryChart from "./UserGoalSummary";
+
+const UserDashboardComponent = () => {
+ return (
+
+ );
+};
+
+export default UserDashboardComponent;
diff --git a/frontend/src/features/UserDashboard/components/UserGoalSummary.tsx b/frontend/src/features/UserDashboard/components/UserGoalSummary.tsx
new file mode 100644
index 00000000..2d668f39
--- /dev/null
+++ b/frontend/src/features/UserDashboard/components/UserGoalSummary.tsx
@@ -0,0 +1,69 @@
+import { useUserGoalSummary } from "@/api/queries/UserGoals";
+import {
+ LineChart,
+ Line,
+ XAxis,
+ YAxis,
+ CartesianGrid,
+ Tooltip,
+ Legend,
+ ResponsiveContainer
+} from "recharts";
+
+const UserGoalSummaryChart = () => {
+ const { data, isLoading, isError } = useUserGoalSummary();
+
+ if (isLoading) return Loading...
;
+ 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 => ({
+ day: new Date(item.snapshotDate).toLocaleDateString("en-GB", {
+ day: "2-digit",
+ month: "short"
+ }),
+ incomplete: item.incompleteGoalsCount,
+ targetSet: item.targetSet,
+ targetCompleted: item.targetCompleted
+ }));
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default UserGoalSummaryChart;
diff --git a/frontend/src/features/UserDashboard/index.tsx b/frontend/src/features/UserDashboard/index.tsx
new file mode 100644
index 00000000..554ee80e
--- /dev/null
+++ b/frontend/src/features/UserDashboard/index.tsx
@@ -0,0 +1,7 @@
+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
new file mode 100644
index 00000000..91621762
--- /dev/null
+++ b/frontend/src/index.css
@@ -0,0 +1,174 @@
+@import "tailwindcss";
+@import "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);
+
+ --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);
+ --cc-app-yellow: oklch(0.9136 0.174 99.92);
+}
+
+@media (prefers-color-scheme: light) {
+ :root {
+ color: #213547;
+ background-color: #ffffff;
+ }
+}
+
+@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-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);
+
+ --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);
+ --color-cc-app-yellow: var(--cc-app-yellow);
+}
+
+.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);
+}
+
+@layer base {
+ * {
+ @apply border-border outline-ring/50;
+ }
+
+ body {
+ @apply bg-background text-foreground;
+ }
+
+ .no-scrollbar::-webkit-scrollbar {
+ display: none;
+ }
+
+ .no-scrollbar {
+ -ms-overflow-style: none;
+ scrollbar-width: none;
+ }
+}
diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx
new file mode 100644
index 00000000..cd015264
--- /dev/null
+++ b/frontend/src/main.tsx
@@ -0,0 +1,17 @@
+import { StrictMode } from "react";
+import { createRoot } from "react-dom/client";
+import { QueryClientProvider } from "@tanstack/react-query";
+import { Toaster } from "sonner";
+
+import Router from "@/root/Router";
+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
new file mode 100644
index 00000000..ccd09460
--- /dev/null
+++ b/frontend/src/root/Router.tsx
@@ -0,0 +1,44 @@
+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";
+import AdminLayout from "@/shared/layout/AdminLayout";
+import AppLayout from "@/shared/layout/AppLayout";
+
+const generateRoutes = (routes: RoutesType[]) => {
+ 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}
+ );
+ }
+
+ if (layout == Layout.AdminLayout) {
+ wrappedElement = {wrappedElement};
+ }
+ wrappedElement = {wrappedElement};
+
+ return { path, element: wrappedElement };
+ });
+};
+
+const Router = () => {
+ const router = createBrowserRouter(generateRoutes(routesConfig));
+ return ;
+};
+
+export default Router;
diff --git a/frontend/src/root/routes-config.tsx b/frontend/src/root/routes-config.tsx
new file mode 100644
index 00000000..15e567f0
--- /dev/null
+++ b/frontend/src/root/routes-config.tsx
@@ -0,0 +1,86 @@
+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 {
+ ACCOUNT_INFO_PATH,
+ ADMIN_LEADERBOARD_PATH,
+ ADMIN_LOGIN_PATH,
+ ADMIN_SCORE_CONFIGURE_PATH,
+ ADMIN_USERS_PATH,
+ LOGIN_PATH,
+ MY_CONTRIBUTIONS_PATH,
+ REPOSITORY_DETAILS_PATH,
+ USER_DASHBOARD_PATH
+} from "@/shared/constants/routes";
+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";
+import Leaderboard from "@/features/UserDashboard/components/Leaderboard";
+import AdminLeaderboard from "@/features/Admin/AdminLeaderboard";
+export interface RoutesType {
+ path: string;
+ element: ReactNode;
+ isProtected?: boolean;
+ layout: LayoutType;
+}
+
+export const routesConfig: RoutesType[] = [
+ {
+ path: LOGIN_PATH,
+ element: ,
+ isProtected: false,
+ layout: Layout.AuthLayout
+ },
+ {
+ path: USER_DASHBOARD_PATH,
+ element: ,
+ isProtected: true,
+ layout: Layout.DashboardLayout
+ },
+ {
+ path: MY_CONTRIBUTIONS_PATH,
+ element: ,
+ isProtected: true,
+ layout: Layout.DashboardLayout
+ },
+ {
+ path: REPOSITORY_DETAILS_PATH,
+ element: ,
+ isProtected: true,
+ layout: Layout.DashboardLayout
+ },
+ {
+ path: ADMIN_LOGIN_PATH,
+ element: ,
+ isProtected: false,
+ layout: Layout.AuthLayout
+ },
+ {
+ path: ADMIN_USERS_PATH,
+ element: ,
+ isProtected: true,
+ layout: Layout.AdminLayout
+ },
+ {
+ path: ADMIN_SCORE_CONFIGURE_PATH,
+ element: ,
+ isProtected: true,
+ layout: Layout.AdminLayout
+ },
+ {
+ path: ADMIN_LEADERBOARD_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
new file mode 100644
index 00000000..171ba7e9
--- /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;
\ 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/DeleteAccount.tsx b/frontend/src/shared/components/UserDashboard/DeleteAccount.tsx
new file mode 100644
index 00000000..8ab0c35d
--- /dev/null
+++ b/frontend/src/shared/components/UserDashboard/DeleteAccount.tsx
@@ -0,0 +1,69 @@
+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";
+import { clearAccessToken } from "@/shared/utils/local-storage";
+import { useNavigate } from "react-router-dom";
+
+interface Props {
+ open: boolean;
+ onClose: () => void;
+}
+
+const DeleteAccount = ({ open, onClose }: Props) => {
+ const navigate = useNavigate();
+ const { data } = useLoggedInUser();
+ const user = data?.data;
+ const { mutate: softDeleteUser, isPending } = useSoftDeleteUser();
+
+ const handleDelete = () => {
+ if (!user?.userId) return;
+ softDeleteUser(user.userId, {
+ onSuccess: () => {
+ onClose();
+ clearAccessToken();
+ navigate("/login");
+ }
+ });
+ };
+
+ return (
+
+ );
+};
+
+export default DeleteAccount;
diff --git a/frontend/src/shared/components/UserDashboard/Navbar.tsx b/frontend/src/shared/components/UserDashboard/Navbar.tsx
new file mode 100644
index 00000000..b660ce34
--- /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/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 (
+
+ );
+};
+
+export default SettingsDialog;
diff --git a/frontend/src/shared/components/UserDashboard/UserBadges.tsx b/frontend/src/shared/components/UserDashboard/UserBadges.tsx
new file mode 100644
index 00000000..9a920266
--- /dev/null
+++ b/frontend/src/shared/components/UserDashboard/UserBadges.tsx
@@ -0,0 +1,56 @@
+import { useUserBadges } from "@/api/queries/UserBadges";
+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,
+ CUSTOM: customBadge
+};
+
+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 badge = badgeColorMap[type] ?? "";
+ return (
+
+

+ {badgeList.length > 1 && (
+
+ ×{badgeList.length}
+
+ )}
+
+ {type}
+
+
+ );
+ })}
+
+
+ );
+};
+
+export default UserBadges;
diff --git a/frontend/src/shared/components/UserDashboard/UserEmail.tsx b/frontend/src/shared/components/UserDashboard/UserEmail.tsx
new file mode 100644
index 00000000..c291dc78
--- /dev/null
+++ b/frontend/src/shared/components/UserDashboard/UserEmail.tsx
@@ -0,0 +1,72 @@
+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";
+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;
+ 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: () => {
+ queryClient.invalidateQueries({
+ queryKey: [LOGGED_IN_USER_QUERY_KEY]
+ });
+ toast.success("email updated successfully");
+ onClose();
+ }
+ });
+ };
+
+ return (
+
+ );
+};
+
+export default UserEmail;
diff --git a/frontend/src/shared/components/UserDashboard/UserGoals.tsx b/frontend/src/shared/components/UserDashboard/UserGoals.tsx
new file mode 100644
index 00000000..46005dd2
--- /dev/null
+++ b/frontend/src/shared/components/UserDashboard/UserGoals.tsx
@@ -0,0 +1,499 @@
+import { useState } from "react";
+import { Progress } from "@/shared/components/ui/progress";
+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,
+ useGoalLevels,
+ useGoalLevelTargets,
+ useResetUserGoalStatus,
+ useSetUserGoalLevel,
+ useUserCurrentGoalStatus
+} from "@/api/queries/UserGoals";
+import { useQueryClient } from "@tanstack/react-query";
+import { USER_ACTIVE_GOAL_LEVEL_QUERY_KEY } from "@/shared/constants/query-keys";
+import type {
+ ContributionTypeDetail,
+ CustomGoalLevelTarget
+} from "@/shared/types/types";
+import { toast } from "sonner";
+import information from "@/assets/information.png";
+
+const UserGoals = () => {
+ const [dialogOpen, setDialogOpen] = useState(false);
+ 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();
+
+ const queryClient = useQueryClient();
+
+ const userLevel = userGoalLevelRes?.data ?? null;
+ const goalLevels = goalLevelsRes?.data ?? [];
+ const goalLevelTargets = goalLevelTargetData?.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);
+
+ if (level.toLowerCase() === "custom") {
+ setDialogOpen(false);
+ setIsCustomDialogOpen(true);
+ setIsSettingLevel(false);
+ return;
+ }
+
+ 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: (err: any) => {
+ const message =
+ err?.response?.data?.message || "Failed to reset goal status";
+ toast.error(message);
+ }
+ });
+ };
+
+ 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 = () => {
+ 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")
+ }
+ );
+ };
+
+ const handleViewLevelTarget = (selectedLevel: {
+ id: number;
+ level: string;
+ createdAt: string;
+ updatedAt: string;
+ }) => {
+ goalLevel(selectedLevel);
+ setSelectedLevel(selectedLevel.level);
+ setLevelTargetDialogOpen(true);
+ };
+
+ if (isGoalLevelLoading || isGoalLevelsLoading) {
+ return (
+
+
+ Loading goals...
+
+ );
+ }
+
+ return (
+
+
+
+
+ MY GOALS {userLevel?.level && `(${userLevel.level.toUpperCase()})`}
+
+
+
+
+ {isWithin48HoursOrGreaterThan30Days(createdAt) && (
+
+ )}
+
+
+ {userLevel ? (
+
+ {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.progress}/{goal.target}
+
+
+
+
+ );
+ })}
+
+ ) : (
+
+
No Active Goal Set
+
+ You haven't selected a goal level yet. Choose a level to start
+ tracking contributions.
+
+
+
+ )}
+
+ {/* Level Target Dialog */}
+
+
+ {/* Custom Goal Dialog */}
+
+
+ );
+};
+
+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..42bed017
--- /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..012bc530
--- /dev/null
+++ b/frontend/src/shared/components/UserDashboard/UserProfileDetails.tsx
@@ -0,0 +1,116 @@
+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 { Separator } from "@/shared/components/ui/separator";
+import { useLoggedInUser } from "@/api/queries/UserProfileDetails";
+import { Link, useNavigate } from "react-router-dom";
+
+import UserProfileMenu from "./UserProfileMenu";
+import UserEmail from "./UserEmail";
+import SettingsDialog from "./SettingsDialog";
+import {
+ clearAccessToken,
+ clearUserCredentials,
+ setUserData
+} from "@/shared/utils/local-storage";
+import { toast } from "sonner";
+import type { AxiosError } from "axios";
+
+const UserProfileDetails = () => {
+ const navigate = useNavigate();
+ const { data, error, isError } = useLoggedInUser();
+ const user = data?.data;
+
+ if (isError) {
+ const axiosError = error as AxiosError<{ message: string }>;
+ toast.error(axiosError.response?.data?.message);
+ clearUserCredentials();
+ navigate("/login");
+ }
+
+ setUserData(user);
+
+ const [showSettingsDialog, setShowSettingsDialog] = useState(false);
+ const [showEmailDialog, setShowEmailDialog] = useState(false);
+
+ const handleSettingsClick = () => setShowSettingsDialog(true);
+ const handleLogoutClick = () => {
+ clearAccessToken();
+ navigate("/login");
+ };
+
+ const handleUpdateEmail = () => {
+ setShowSettingsDialog(false);
+ setShowEmailDialog(true);
+ };
+
+ return (
+
+
+
+
+
+

+
+
+
+
+
+
+
+
+
+
+ {user?.githubUsername || "username"}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {user?.currentBalance || "0"}
+
+
+
+
+
+
+
+
+
setShowSettingsDialog(false)}
+ onUpdateEmail={handleUpdateEmail}
+ />
+ {showEmailDialog && (
+ setShowEmailDialog(false)}
+ />
+ )}
+
+ );
+};
+
+export default UserProfileDetails;
diff --git a/frontend/src/shared/components/UserDashboard/UserProfileMenu.tsx b/frontend/src/shared/components/UserDashboard/UserProfileMenu.tsx
new file mode 100644
index 00000000..b62108d5
--- /dev/null
+++ b/frontend/src/shared/components/UserDashboard/UserProfileMenu.tsx
@@ -0,0 +1,47 @@
+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
new file mode 100644
index 00000000..ad15b4e4
--- /dev/null
+++ b/frontend/src/shared/components/common/ActivityCard.tsx
@@ -0,0 +1,61 @@
+import type { FC } from "react";
+import Coin from "@/shared/components/common/Coin";
+import { format } from "date-fns";
+
+interface ActivityCardProps {
+ contributionType: string;
+ repositoryName?: string;
+ contributedAt: string;
+ balanceChange: number;
+ showLine: boolean;
+ isRepositoryActivity?: boolean;
+}
+
+const ActivityCard: FC = ({
+ contributionType,
+ repositoryName,
+ contributedAt,
+ balanceChange,
+ showLine = true,
+ isRepositoryActivity
+}) => {
+ return (
+
+ {showLine && (
+
+ )}
+
+
+
+
+
+
+ {contributionType.replace(/([A-Z])/g, " $1")}
+
+ {isRepositoryActivity ? null : (
+
+ Contributed to <{repositoryName}>
+
+ )}
+
+
+ Contributed on {format(new Date(contributedAt), "MMM d yyyy")}
+
+
+
+ {balanceChange && (
+
+
+ {balanceChange < 0 ? `-${Math.abs(balanceChange)}` : balanceChange}
+
+ )}
+
+
+ );
+};
+
+export default ActivityCard;
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/components/common/Coin.tsx b/frontend/src/shared/components/common/Coin.tsx
new file mode 100644
index 00000000..0ba372f8
--- /dev/null
+++ b/frontend/src/shared/components/common/Coin.tsx
@@ -0,0 +1,9 @@
+import coinSvg from "@/assets/Coin.svg";
+
+const Coin = () => {
+ return (
+
+ );
+};
+
+export default 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..5dd3e573
--- /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 (
+
+ );
+};
+
+export default CoinsInfo;
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/button.tsx b/frontend/src/shared/components/ui/button.tsx
new file mode 100644
index 00000000..5fc0eaeb
--- /dev/null
+++ b/frontend/src/shared/components/ui/button.tsx
@@ -0,0 +1,67 @@
+import * as React from "react";
+import { type VariantProps, cva } from "class-variance-authority";
+import { Slot } from "@radix-ui/react-slot";
+
+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: {
+ primary:
+ "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 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 hover:cursor-pointer",
+ ghost:
+ "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 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:
+ "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",
+ 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: "primary",
+ size: "md"
+ }
+ }
+);
+
+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/shared/components/ui/card.tsx b/frontend/src/shared/components/ui/card.tsx
new file mode 100644
index 00000000..f27e1678
--- /dev/null
+++ b/frontend/src/shared/components/ui/card.tsx
@@ -0,0 +1,92 @@
+import * as React from "react";
+
+import { cn } from "@/shared/utils/tailwindcss";
+
+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,
+ CardAction,
+ CardContent,
+ CardDescription,
+ CardFooter,
+ CardHeader,
+ CardTitle
+};
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/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/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/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/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/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/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/constants/constants.ts b/frontend/src/shared/constants/constants.ts
new file mode 100644
index 00000000..ecf83ebf
--- /dev/null
+++ b/frontend/src/shared/constants/constants.ts
@@ -0,0 +1,27 @@
+export const AuthLayoutDetails = [
+ "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 = {
+ 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/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/layout.ts b/frontend/src/shared/constants/layout.ts
new file mode 100644
index 00000000..7b59a8d1
--- /dev/null
+++ b/frontend/src/shared/constants/layout.ts
@@ -0,0 +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/local-storage.ts b/frontend/src/shared/constants/local-storage.ts
new file mode 100644
index 00000000..7d1daf7c
--- /dev/null
+++ b/frontend/src/shared/constants/local-storage.ts
@@ -0,0 +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
new file mode 100644
index 00000000..52a99424
--- /dev/null
+++ b/frontend/src/shared/constants/query-keys.ts
@@ -0,0 +1,21 @@
+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 GOAL_LEVEL_TARGETS_QUERY_KEY = "goal-level-targets";
+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 USER_GOAL_LEVEL_SUMMARY_QUERY_KEY="user-goal-level-summary"
+
+export const GET_ALL_USERS_QUERY_KEY = "get-all-users";
\ 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..6864ca58
--- /dev/null
+++ b/frontend/src/shared/constants/routes.ts
@@ -0,0 +1,10 @@
+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";
+export const ADMIN_LEADERBOARD_PATH="/admin/leaderboard"
diff --git a/frontend/src/shared/layout/AdminLayout.tsx b/frontend/src/shared/layout/AdminLayout.tsx
new file mode 100644
index 00000000..ba043533
--- /dev/null
+++ b/frontend/src/shared/layout/AdminLayout.tsx
@@ -0,0 +1,162 @@
+import React, { type FC, type ReactNode } from "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";
+import { clearAccessToken } from "../utils/local-storage";
+
+interface AdminLayoutProps {
+ children: ReactNode;
+}
+
+const AdminLayout: FC = ({ children }) => {
+ const [activeItem, setActiveItem] = React.useState("Users");
+ const navigate = useNavigate();
+
+ const menuItems = [
+ {
+ name: "Configure Score",
+ icon: BarChart3,
+ path: "/admin/configure/score"
+ },
+ { name: "Users", icon: Users, path: "/admin/users" },
+ { name: "View Leaderboard", icon: Trophy, path: "/admin/leaderboard" }
+ ];
+
+ const handleMenuClick = (itemName: string, path: string) => {
+ setActiveItem(itemName);
+ navigate(path);
+ };
+
+ return (
+
+
+
+
+
+
+ {Array.from({ length: 24 }).map((_, i) => (
+
+ ))}
+
+
+
+
+
+ </>
+
+
+
+ CODE CURIOSITY
+
+
+ Admin Portal
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {activeItem}
+
+
+ Manage your {activeItem.toLowerCase()} settings and
+ configurations
+
+
+
+
+
+
+
+
+
+
+
+ Admin
+
+ Administrator
+
+
+
+
+
+
+
+ {children}
+
+
+
+ );
+};
+
+export default AdminLayout;
diff --git a/frontend/src/shared/layout/AppLayout.tsx b/frontend/src/shared/layout/AppLayout.tsx
new file mode 100644
index 00000000..b41c295f
--- /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 (
+
+ );
+};
+
+export default AppLayout;
diff --git a/frontend/src/shared/layout/AuthLayout.tsx b/frontend/src/shared/layout/AuthLayout.tsx
new file mode 100644
index 00000000..8f7d7750
--- /dev/null
+++ b/frontend/src/shared/layout/AuthLayout.tsx
@@ -0,0 +1,96 @@
+import { type FC, type ReactNode, useEffect } from "react";
+import { Link, 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";
+import { AuthLayoutDetails } from "../constants/constants";
+
+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
+
+
+
+
+ {AuthLayoutDetails.map((text, i) => (
+
+
+
+ {text}
+
+
+ ))}
+
+
+
+
+
+
+
+ {Array.from({ length: 64 }).map((_, i) => (
+
+ ))}
+
+
+
+
+ {children}
+ {location.pathname.startsWith("/admin") ? (
+
+ Log in as user?
+
+ ) : (
+
+ {/* Log in as admin? */}
+
+ )}
+
+
+
+ );
+};
+
+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 (
+
+ );
+};
+
+export default UserDashboardLayout;
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/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/types/navbar.ts b/frontend/src/shared/types/navbar.ts
new file mode 100644
index 00000000..752aba1c
--- /dev/null
+++ b/frontend/src/shared/types/navbar.ts
@@ -0,0 +1,9 @@
+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..2929b54d
--- /dev/null
+++ b/frontend/src/shared/types/types.ts
@@ -0,0 +1,238 @@
+export interface User {
+ userId: number;
+ githubId: number;
+ githubUsername: string;
+ email: string;
+ avatarUrl: string;
+ currentBalance: 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;
+}
+
+export interface Repositories {
+ id: number;
+ githubRepoId: number;
+ repoName: string;
+ description: string;
+ languagesUrl: string;
+ repoUrl: string;
+ ownerName: string;
+ updateDate: string;
+ contributorsUrl: string;
+ createdAt: string;
+ updatedAt: string;
+ languages: string[];
+ totalCoinsEarned: number;
+}
+
+export interface Language {
+ name: string;
+ bytes: number;
+ percentage: number;
+}
+
+export interface Repository {
+ id: number;
+ githubRepoId: number;
+ repoName: string;
+ description: string;
+ languagesUrl: string;
+ repoUrl: string;
+ ownerName: string;
+ updateDate: string;
+ contributorsUrl: string;
+ createdAt: string;
+ updatedAt: string;
+ languages: string[];
+}
+
+export interface Contributor {
+ id: number;
+ name: string;
+ avatar_url: string;
+ github_url: string;
+ contributions: number;
+}
+
+export interface RepositoryActivity {
+ id: number;
+ userId: number;
+ repositoryId: number;
+ contributionScoreId: number;
+ contributionType: string;
+ balanceChange: number;
+ contributedAt: string;
+ githubEventId: string;
+ createdAt: string;
+ updatedAt: string;
+}
+
+export interface GoalLevel {
+ id: number;
+ level: string;
+ createdAt: string;
+ updatedAt: string;
+}
+
+export interface GoalLevelTarget {
+ id: number;
+ goalLevelId: number;
+ contributionType: string;
+ target: number;
+ createdAt: string;
+ updatedAt: string;
+}
+
+export interface UserGoalTargetProgress {
+ contributionType: string;
+ target: number;
+ progress: number;
+}
+
+export interface UserCurrentGoalStatus {
+ userGoalId: number;
+ level: string;
+ status: string;
+ monthStartedAt: string;
+ createdAt: string;
+ updatedAt: string;
+ goalTargetProgress: UserGoalTargetProgress[];
+}
+
+export interface UserGoal {
+ id: number;
+ userId: number;
+ goalId: number;
+ status: string;
+ createdAt: string;
+ updatedAt: string;
+}
+
+export interface SetUserGoalLevelRequest {
+ level: string;
+ customTargets: CustomGoalLevelTarget[];
+}
+
+export interface CustomGoalLevelTarget {
+ contributionType: string;
+ target: number;
+}
+
+export interface ContributionTypeDetail {
+ id: number;
+ adminId: number;
+ contributionType: string;
+ score: number;
+ createdAt: string;
+ updatedAt: string;
+}
+
+export interface UserGoalLevelStatus {
+ id: number;
+ userId: number;
+ goalId: number;
+ status: string;
+ 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;
+}
+
+export interface ContributionScore {
+ id: number;
+ adminId: number;
+ contributionType: string;
+ score: number;
+ createdAt: string;
+ updatedAt: string;
+}
+
+export interface ContributionScoreUpdate {
+ contributionType: string;
+ score: number;
+}
+
+export interface GoalSummary {
+ id: number;
+ userId: number;
+ snapshotDate: string;
+ incompleteGoalsCount: number;
+ targetSet: number;
+ targetCompleted: number;
+ createdAt: string;
+ updatedAt: string;
+}
diff --git a/frontend/src/shared/utils/local-storage.ts b/frontend/src/shared/utils/local-storage.ts
new file mode 100644
index 00000000..41b0c8a5
--- /dev/null
+++ b/frontend/src/shared/utils/local-storage.ts
@@ -0,0 +1,33 @@
+import { ACCESS_TOKEN_KEY, USER_DATA_KEY } from "@/shared/constants/local-storage";
+import type { User } from "../types/types";
+
+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 clearAccessToken = () => {
+ localStorage.removeItem(ACCESS_TOKEN_KEY);
+};
+
+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: any) => {
+ localStorage.setItem(USER_DATA_KEY, JSON.stringify(data));
+};
+
+export const clearUserCredentials = () => {
+ localStorage.removeItem(USER_DATA_KEY);
+ localStorage.removeItem(ACCESS_TOKEN_KEY);
+};
\ No newline at end of file
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/vite-env.d.ts b/frontend/src/vite-env.d.ts
new file mode 100644
index 00000000..776744a6
--- /dev/null
+++ b/frontend/src/vite-env.d.ts
@@ -0,0 +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..f7c4e4d7
--- /dev/null
+++ b/frontend/tailwind.config.js
@@ -0,0 +1,64 @@
+/** @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-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))",
+ ccappyellow: "hsl(var(--cc-app-yellow))",
+
+ 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
new file mode 100644
index 00000000..f39faca8
--- /dev/null
+++ b/frontend/tsconfig.app.json
@@ -0,0 +1,31 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
+ "target": "ES2022",
+ "useDefineForClassFields": true,
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["./src/*"]
+ },
+
+ /* 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..f916da50
--- /dev/null
+++ b/frontend/vite.config.ts
@@ -0,0 +1,13 @@
+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")
+ }
+ }
+});
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/auth/domain.go b/internal/app/auth/domain.go
deleted file mode 100644
index 00f61f9a..00000000
--- a/internal/app/auth/domain.go
+++ /dev/null
@@ -1,40 +0,0 @@
-package auth
-
-import (
- "database/sql"
- "time"
-)
-
-const (
- LoginWithGithubFailed = "LoginWithGithubFailed"
- AccessTokenCookieName = "AccessToken"
- GitHubOAuthState = "state"
- GithubOauthScope = "read:user"
- GetUserGithubUrl = "https://api.github.com/user"
- GetUserEmailUrl = "https://api.github.com/user/emails"
-)
-
-type User struct {
- Id int `json:"user_id"`
- GithubId int `json:"github_id"`
- GithubUsername string `json:"github_username"`
- 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"`
- 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"`
-}
-
-type GithubUserResponse struct {
- GithubId int `json:"id"`
- GithubUsername string `json:"login"`
- AvatarUrl string `json:"avatar_url"`
- Email string `json:"email"`
- IsAdmin bool `json:"is_admin"`
-}
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/domain.go b/internal/app/user/domain.go
deleted file mode 100644
index e2d9e6c7..00000000
--- a/internal/app/user/domain.go
+++ /dev/null
@@ -1,35 +0,0 @@
-package user
-
-import (
- "database/sql"
- "time"
-)
-
-type User struct {
- Id int `json:"user_id"`
- GithubId int `json:"github_id"`
- GithubUsername string `json:"github_username"`
- 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"`
- 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"`
-}
-
-type CreateUserRequestBody struct {
- GithubId int `json:"id"`
- GithubUsername string `json:"github_id"`
- AvatarUrl string `json:"avatar_url"`
- Email string `json:"email"`
- IsAdmin bool `json:"is_admin"`
-}
-
-type Email struct {
- Email string `json:"email"`
-}
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
-}
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": {}
+}
| |