diff --git a/.gitignore b/.gitignore
index 9890de43..0197edab 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,27 @@
-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/cmd/main.go b/backend/cmd/main.go
similarity index 73%
rename from cmd/main.go
rename to backend/cmd/main.go
index 35ae97a7..c0fc99e0 100644
--- a/cmd/main.go
+++ b/backend/cmd/main.go
@@ -6,25 +6,25 @@ import (
"log/slog"
"net/http"
"os"
-
+
"os/signal"
"syscall"
"time"
"github.com/joshsoftware/code-curiosity-2025/internal/app"
+ "github.com/joshsoftware/code-curiosity-2025/internal/app/cronJob"
"github.com/joshsoftware/code-curiosity-2025/internal/config"
)
func main() {
ctx := context.Background()
- cfg,err := config.LoadAppConfig()
+ cfg, err := config.LoadAppConfig()
if err != nil {
slog.Error("error loading app config", "error", err)
return
}
-
db, err := config.InitDataStore(cfg)
if err != nil {
slog.Error("error initializing database", "error", err)
@@ -32,10 +32,21 @@ func main() {
}
defer db.Close()
- dependencies := app.InitDependencies(db,cfg)
+ bigqueryInstance, err := config.BigqueryInit(ctx, cfg)
+ if err != nil {
+ slog.Error("error initializing bigquery", "error", err)
+ return
+ }
+
+ httpClient := &http.Client{}
+
+ dependencies := app.InitDependencies(db, cfg, bigqueryInstance, httpClient)
router := app.NewRouter(dependencies)
+ newCronSchedular := cronJob.NewCronSchedular()
+ newCronSchedular.InitCronJobs(dependencies.ContributionService, dependencies.UserService)
+
server := http.Server{
Addr: fmt.Sprintf(":%s", cfg.HTTPServer.Port),
Handler: router,
diff --git a/backend/go.mod b/backend/go.mod
new file mode 100644
index 00000000..e73f2e26
--- /dev/null
+++ b/backend/go.mod
@@ -0,0 +1,65 @@
+module github.com/joshsoftware/code-curiosity-2025
+
+go 1.23.4
+
+require (
+ cloud.google.com/go/bigquery v1.68.0
+ github.com/golang-jwt/jwt/v4 v4.5.2
+ github.com/golang-migrate/migrate/v4 v4.18.3
+ github.com/ilyakaznacheev/cleanenv v1.5.0
+ github.com/jmoiron/sqlx v1.4.0
+ github.com/lib/pq v1.10.9
+ github.com/robfig/cron/v3 v3.0.1
+ golang.org/x/oauth2 v0.29.0
+ google.golang.org/api v0.231.0
+)
+
+require (
+ cloud.google.com/go v0.121.0 // indirect
+ cloud.google.com/go/auth v0.16.1 // indirect
+ cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
+ cloud.google.com/go/compute/metadata v0.6.0 // indirect
+ cloud.google.com/go/iam v1.5.2 // indirect
+ github.com/BurntSushi/toml v1.2.1 // indirect
+ github.com/apache/arrow/go/v15 v15.0.2 // indirect
+ github.com/felixge/httpsnoop v1.0.4 // indirect
+ github.com/go-logr/logr v1.4.2 // indirect
+ github.com/go-logr/stdr v1.2.2 // indirect
+ github.com/goccy/go-json v0.10.2 // indirect
+ github.com/google/flatbuffers v23.5.26+incompatible // indirect
+ github.com/google/s2a-go v0.1.9 // indirect
+ github.com/google/uuid v1.6.0 // indirect
+ github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
+ github.com/googleapis/gax-go/v2 v2.14.1 // indirect
+ github.com/hashicorp/errwrap v1.1.0 // indirect
+ github.com/hashicorp/go-multierror v1.1.1 // indirect
+ github.com/joho/godotenv v1.5.1 // indirect
+ github.com/klauspost/compress v1.16.7 // indirect
+ github.com/klauspost/cpuid/v2 v2.2.5 // indirect
+ github.com/pierrec/lz4/v4 v4.1.18 // indirect
+ github.com/zeebo/xxh3 v1.0.2 // indirect
+ go.opentelemetry.io/auto/sdk v1.1.0 // indirect
+ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect
+ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
+ go.opentelemetry.io/otel v1.35.0 // indirect
+ go.opentelemetry.io/otel/metric v1.35.0 // indirect
+ go.opentelemetry.io/otel/trace v1.35.0 // indirect
+ go.uber.org/atomic v1.11.0 // indirect
+ golang.org/x/crypto v0.37.0 // indirect
+ golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
+ golang.org/x/mod v0.23.0 // indirect
+ golang.org/x/net v0.39.0 // indirect
+ golang.org/x/sync v0.14.0 // indirect
+ golang.org/x/sys v0.32.0 // indirect
+ golang.org/x/text v0.24.0 // indirect
+ golang.org/x/time v0.11.0 // indirect
+ golang.org/x/tools v0.30.0 // indirect
+ golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
+ google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb // indirect
+ google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34 // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 // indirect
+ google.golang.org/grpc v1.72.0 // indirect
+ google.golang.org/protobuf v1.36.6 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+ olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 // indirect
+)
diff --git a/backend/go.sum b/backend/go.sum
new file mode 100644
index 00000000..a5992483
--- /dev/null
+++ b/backend/go.sum
@@ -0,0 +1,213 @@
+cel.dev/expr v0.20.0 h1:OunBvVCfvpWlt4dN7zg3FM6TDkzOePe1+foGJ9AXeeI=
+cel.dev/expr v0.20.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw=
+cloud.google.com/go v0.121.0 h1:pgfwva8nGw7vivjZiRfrmglGWiCJBP+0OmDpenG/Fwg=
+cloud.google.com/go v0.121.0/go.mod h1:rS7Kytwheu/y9buoDmu5EIpMMCI4Mb8ND4aeN4Vwj7Q=
+cloud.google.com/go/auth v0.16.1 h1:XrXauHMd30LhQYVRHLGvJiYeczweKQXZxsTbV9TiguU=
+cloud.google.com/go/auth v0.16.1/go.mod h1:1howDHJ5IETh/LwYs3ZxvlkXF48aSqqJUM+5o02dNOI=
+cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
+cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
+cloud.google.com/go/bigquery v1.68.0 h1:F+CPqdcMxZGUDBACzGtOJ1E6E0MWSYcKeFthxnhpYIU=
+cloud.google.com/go/bigquery v1.68.0/go.mod h1:1UAksG8IFXJomQV38xUsRB+2m2c1H9U0etvoGHgyhDk=
+cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=
+cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
+cloud.google.com/go/datacatalog v1.26.0 h1:eFgygb3DTufTWWUB8ARk+dSuXz+aefNJXTlkWlQcWwE=
+cloud.google.com/go/datacatalog v1.26.0/go.mod h1:bLN2HLBAwB3kLTFT5ZKLHVPj/weNz6bR0c7nYp0LE14=
+cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8=
+cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE=
+cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE=
+cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY=
+cloud.google.com/go/monitoring v1.24.0 h1:csSKiCJ+WVRgNkRzzz3BPoGjFhjPY23ZTcaenToJxMM=
+cloud.google.com/go/monitoring v1.24.0/go.mod h1:Bd1PRK5bmQBQNnuGwHBfUamAV1ys9049oEPHnn4pcsc=
+cloud.google.com/go/storage v1.53.0 h1:gg0ERZwL17pJ+Cz3cD2qS60w1WMDnwcm5YPAIQBHUAw=
+cloud.google.com/go/storage v1.53.0/go.mod h1:7/eO2a/srr9ImZW9k5uufcNahT2+fPb8w5it1i5boaA=
+filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
+filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
+github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
+github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
+github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
+github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 h1:ErKg/3iS1AKcTkf3yixlZ54f9U1rljCkQyEXWUnIUxc=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0/go.mod h1:yAZHSGnqScoU556rBOVkwLze6WP5N+U11RHuWaGVxwY=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 h1:fYE9p3esPxA/C0rQ0AHhP0drtPXDRhaWiwg1DPqO7IU=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0/go.mod h1:BnBReJLvVYx2CS/UHOgVz2BXKXD9wsQPxZug20nZhd0=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 h1:6/0iUd0xrnX7qt+mLNRwg5c0PGv8wpE8K90ryANQwMI=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0/go.mod h1:otE2jQekW/PqXk1Awf5lmfokJx4uwuqcj1ab5SpGeW0=
+github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
+github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
+github.com/apache/arrow/go/v15 v15.0.2 h1:60IliRbiyTWCWjERBCkO1W4Qun9svcYoZrSLcyOsMLE=
+github.com/apache/arrow/go/v15 v15.0.2/go.mod h1:DGXsR3ajT524njufqf95822i+KTh+yea1jass9YXgjA=
+github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
+github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42 h1:Om6kYQYDUk5wWbT0t0q6pvyM49i9XZAv9dDrkDA7gjk=
+github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dhui/dktest v0.4.5 h1:uUfYBIVREmj/Rw6MvgmqNAYzTiKOHJak+enB5Di73MM=
+github.com/dhui/dktest v0.4.5/go.mod h1:tmcyeHDKagvlDrz7gDKq4UAJOLIfVZYkfD5OnHDwcCo=
+github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
+github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
+github.com/docker/docker v27.2.0+incompatible h1:Rk9nIVdfH3+Vz4cyI/uhbINhEZ/oLmc+CBXmH6fbNk4=
+github.com/docker/docker v27.2.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
+github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
+github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
+github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
+github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M=
+github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A=
+github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw=
+github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8=
+github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU=
+github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
+github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
+github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E=
+github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc=
+github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
+github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
+github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
+github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
+github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
+github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
+github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
+github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
+github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
+github.com/golang-migrate/migrate/v4 v4.18.3 h1:EYGkoOsvgHHfm5U/naS1RP/6PL/Xv3S4B/swMiAmDLs=
+github.com/golang-migrate/migrate/v4 v4.18.3/go.mod h1:99BKpIi6ruaaXRM1A77eqZ+FWPQ3cfRa+ZVy5bmWMaY=
+github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
+github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
+github.com/google/flatbuffers v23.5.26+incompatible h1:M9dgRyhJemaM4Sw8+66GHBu8ioaQmyPLg1b8VwK5WJg=
+github.com/google/flatbuffers v23.5.26+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc=
+github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0=
+github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
+github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4=
+github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
+github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q=
+github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA=
+github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
+github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
+github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
+github.com/ilyakaznacheev/cleanenv v1.5.0 h1:0VNZXggJE2OYdXE87bfSSwGxeiGt9moSR2lOrsHHvr4=
+github.com/ilyakaznacheev/cleanenv v1.5.0/go.mod h1:a5aDzaJrLCQZsazHol1w8InnDcOX0OColm64SlIi6gk=
+github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
+github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
+github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
+github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
+github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
+github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
+github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
+github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
+github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
+github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
+github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
+github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
+github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
+github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
+github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
+github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
+github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
+github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
+github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
+github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
+github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
+github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ=
+github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
+github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
+github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
+github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
+github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
+github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE=
+github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
+github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
+github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM=
+github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4=
+github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
+github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
+go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
+go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
+go.opentelemetry.io/contrib/detectors/gcp v1.35.0 h1:bGvFt68+KTiAKFlacHW6AhA56GF2rS0bdD3aJYEnmzA=
+go.opentelemetry.io/contrib/detectors/gcp v1.35.0/go.mod h1:qGWP8/+ILwMRIUf9uIVLloR1uo5ZYAslM4O6OqUi1DA=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ=
+go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
+go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
+go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
+go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
+go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
+go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
+go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
+go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
+go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
+go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
+go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
+go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
+golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
+golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
+golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
+golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
+golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM=
+golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
+golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
+golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
+golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98=
+golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
+golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
+golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
+golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
+golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
+golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
+golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
+golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY=
+golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY=
+golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY=
+golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
+gonum.org/v1/gonum v0.12.0 h1:xKuo6hzt+gMav00meVPUlXwSdoEJP46BR+wdxQEFK2o=
+gonum.org/v1/gonum v0.12.0/go.mod h1:73TDxJfAAHeA8Mk9mf8NlIppyhQNo5GLTcYeqgo2lvY=
+google.golang.org/api v0.231.0 h1:LbUD5FUl0C4qwia2bjXhCMH65yz1MLPzA/0OYEsYY7Q=
+google.golang.org/api v0.231.0/go.mod h1:H52180fPI/QQlUc0F4xWfGZILdv09GCWKt2bcsn164A=
+google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb h1:ITgPrl429bc6+2ZraNSzMDk3I95nmQln2fuPstKwFDE=
+google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:sAo5UzpjUwgFBCzupwhcLcxHVDK7vG5IqI30YnwX2eE=
+google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34 h1:0PeQib/pH3nB/5pEmFeVQJotzGohV0dq4Vcp09H5yhE=
+google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34/go.mod h1:0awUlEkap+Pb1UMeJwJQQAdJQrt3moU7J2moTy69irI=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 h1:h6p3mQqrmT1XkHVTfzLdNz1u7IhINeZkz67/xTbOuWs=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
+google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM=
+google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
+google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
+google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 h1:slmdOY3vp8a7KQbHkL+FLbvbkgMqmXojpFUO/jENuqQ=
+olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3/go.mod h1:oVgVk4OWVDi43qWBEyGhXgYxt7+ED4iYNpTngSLX2Iw=
diff --git a/backend/internal/app/auth/domain.go b/backend/internal/app/auth/domain.go
new file mode 100644
index 00000000..33f8f994
--- /dev/null
+++ b/backend/internal/app/auth/domain.go
@@ -0,0 +1,41 @@
+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:"avatarUrl"`
+ Email string `json:"email"`
+ IsAdmin bool `json:"isAdmin"`
+}
+
diff --git a/internal/app/auth/handler.go b/backend/internal/app/auth/handler.go
similarity index 80%
rename from internal/app/auth/handler.go
rename to backend/internal/app/auth/handler.go
index 2be51640..3d40c859 100644
--- a/internal/app/auth/handler.go
+++ b/backend/internal/app/auth/handler.go
@@ -7,12 +7,13 @@ import (
"github.com/joshsoftware/code-curiosity-2025/internal/config"
"github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors"
+ "github.com/joshsoftware/code-curiosity-2025/internal/pkg/middleware"
"github.com/joshsoftware/code-curiosity-2025/internal/pkg/response"
)
type handler struct {
authService Service
- appConfig config.AppConfig
+ appConfig config.AppConfig
}
type Handler interface {
@@ -24,7 +25,7 @@ type Handler interface {
func NewHandler(authService Service, appConfig config.AppConfig) Handler {
return &handler{
authService: authService,
- appConfig: appConfig,
+ appConfig: appConfig,
}
}
@@ -62,7 +63,16 @@ func (h *handler) GithubOAuthLoginCallback(w http.ResponseWriter, r *http.Reques
func (h *handler) GetLoggedInUser(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
- userInfo, err := h.authService.GetLoggedInUser(ctx)
+ userIdValue := ctx.Value(middleware.UserIdKey)
+ userId, ok := userIdValue.(int)
+ if !ok {
+ slog.Error("error obtaining user id from context")
+ status, errorMessage := apperrors.MapError(apperrors.ErrContextValue)
+ response.WriteJson(w, status, errorMessage, nil)
+ return
+ }
+
+ userInfo, err := h.authService.GetLoggedInUser(ctx, userId)
if err != nil {
slog.Error("error getting logged in user")
status, errorMessage := apperrors.MapError(err)
diff --git a/internal/app/auth/service.go b/backend/internal/app/auth/service.go
similarity index 87%
rename from internal/app/auth/service.go
rename to backend/internal/app/auth/service.go
index 3bca6c0d..abcaf195 100644
--- a/internal/app/auth/service.go
+++ b/backend/internal/app/auth/service.go
@@ -9,7 +9,6 @@ import (
"github.com/joshsoftware/code-curiosity-2025/internal/config"
"github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors"
"github.com/joshsoftware/code-curiosity-2025/internal/pkg/jwt"
- "github.com/joshsoftware/code-curiosity-2025/internal/pkg/middleware"
"golang.org/x/oauth2"
"golang.org/x/oauth2/github"
)
@@ -23,7 +22,7 @@ type service struct {
type Service interface {
GithubOAuthLoginUrl(ctx context.Context) string
GithubOAuthLoginCallback(ctx context.Context, code string) (string, error)
- GetLoggedInUser(ctx context.Context) (User, error)
+ GetLoggedInUser(ctx context.Context, userId int) (User, error)
}
func NewService(userService user.Service, appCfg config.AppConfig) Service {
@@ -83,18 +82,18 @@ func (s *service) GithubOAuthLoginCallback(ctx context.Context, code string) (st
return "", apperrors.ErrInternalServer
}
+ if userData.IsDeleted {
+ err = s.userService.RecoverAccountInGracePeriod(ctx, userData.Id)
+ if err != nil {
+ slog.Error("error in recovering account in grace period during login", "error", err)
+ return "", apperrors.ErrInternalServer
+ }
+ }
+
return jwtToken, nil
}
-func (s *service) GetLoggedInUser(ctx context.Context) (User, error) {
- userIdValue := ctx.Value(middleware.UserIdKey)
-
- userId, ok := userIdValue.(int)
- if !ok {
- slog.Error("error obtaining user id from context")
- return User{}, apperrors.ErrInternalServer
- }
-
+func (s *service) GetLoggedInUser(ctx context.Context, userId int) (User, error) {
user, err := s.userService.GetUserById(ctx, userId)
if err != nil {
slog.Error("failed to get logged in user", "error", err)
diff --git a/backend/internal/app/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..bedd595d
--- /dev/null
+++ b/backend/internal/app/bigquery/domain.go
@@ -0,0 +1,44 @@
+package bigquery
+
+import "time"
+
+const DailyQuery = `SELECT
+ id,
+ type,
+ public,
+ actor.id AS actor_id,
+ actor.login AS actor_login,
+ actor.gravatar_id AS actor_gravatar_id,
+ actor.url AS actor_url,
+ actor.avatar_url AS actor_avatar_url,
+ repo.id AS repo_id,
+ repo.name AS repo_name,
+ repo.url AS repo_url,
+ payload,
+ created_at,
+ other
+FROM
+ githubarchive.day.%s
+WHERE
+ type IN (
+ 'IssuesEvent',
+ 'PullRequestEvent',
+ 'PullRequestReviewEvent',
+ 'IssueCommentEvent',
+ 'PullRequestReviewCommentEvent'
+ )
+ AND (
+ actor.id IN (%s)
+ )`
+
+type ContributionResponse struct {
+ ID string `bigquery:"id"`
+ Type string `bigquery:"type"`
+ ActorID int `bigquery:"actor_id"`
+ ActorLogin string `bigquery:"actor_login"`
+ RepoID int `bigquery:"repo_id"`
+ RepoName string `bigquery:"repo_name"`
+ RepoUrl string `bigquery:"repo_url"`
+ Payload string `bigquery:"payload"`
+ CreatedAt time.Time `bigquery:"created_at"`
+}
diff --git a/backend/internal/app/bigquery/service.go b/backend/internal/app/bigquery/service.go
new file mode 100644
index 00000000..21a20644
--- /dev/null
+++ b/backend/internal/app/bigquery/service.go
@@ -0,0 +1,53 @@
+package bigquery
+
+import (
+ "context"
+ "fmt"
+ "log/slog"
+ "time"
+
+ bq "cloud.google.com/go/bigquery"
+ "github.com/joshsoftware/code-curiosity-2025/internal/config"
+ "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors"
+ "github.com/joshsoftware/code-curiosity-2025/internal/pkg/utils"
+ "github.com/joshsoftware/code-curiosity-2025/internal/repository"
+)
+
+type service struct {
+ bigqueryInstance config.Bigquery
+ userRepository repository.UserRepository
+}
+
+type Service interface {
+ FetchDailyContributions(ctx context.Context) (*bq.RowIterator, error)
+}
+
+func NewService(bigqueryInstance config.Bigquery, userRepository repository.UserRepository) Service {
+ return &service{
+ bigqueryInstance: bigqueryInstance,
+ userRepository: userRepository,
+ }
+}
+
+func (s *service) FetchDailyContributions(ctx context.Context) (*bq.RowIterator, error) {
+ usersGithubId, err := s.userRepository.GetAllUsersGithubId(ctx, nil)
+ if err != nil {
+ slog.Error("error fetching users github usernames")
+ return nil, apperrors.ErrInternalServer
+ }
+
+ formattedGithubIds := utils.FormatIntSliceForQuery(usersGithubId)
+
+ YesterdayDate := time.Now().AddDate(0, 0, -1)
+ YesterdayYearMonthDay := YesterdayDate.Format("20060102")
+ fetchDailyContributionsQuery := fmt.Sprintf(DailyQuery, YesterdayYearMonthDay, formattedGithubIds)
+
+ bigqueryQuery := s.bigqueryInstance.Client.Query(fetchDailyContributionsQuery)
+ contributionRows, err := bigqueryQuery.Read(ctx)
+ if err != nil {
+ slog.Error("error fetching contributions", "error", err)
+ return nil, err
+ }
+
+ return contributionRows, err
+}
diff --git a/backend/internal/app/contribution/domain.go b/backend/internal/app/contribution/domain.go
new file mode 100644
index 00000000..daaea56b
--- /dev/null
+++ b/backend/internal/app/contribution/domain.go
@@ -0,0 +1,75 @@
+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
+}
diff --git a/backend/internal/app/contribution/handler.go b/backend/internal/app/contribution/handler.go
new file mode 100644
index 00000000..49ea7bf3
--- /dev/null
+++ b/backend/internal/app/contribution/handler.go
@@ -0,0 +1,90 @@
+package contribution
+
+import (
+ "log/slog"
+ "net/http"
+
+ "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors"
+ "github.com/joshsoftware/code-curiosity-2025/internal/pkg/middleware"
+ "github.com/joshsoftware/code-curiosity-2025/internal/pkg/response"
+ "github.com/joshsoftware/code-curiosity-2025/internal/pkg/utils"
+)
+
+type handler struct {
+ contributionService Service
+}
+
+type Handler interface {
+ FetchUserContributions(w http.ResponseWriter, r *http.Request)
+ ListMonthlyContributionSummary(w http.ResponseWriter, r *http.Request)
+}
+
+func NewHandler(contributionService Service) Handler {
+ return &handler{
+ contributionService: contributionService,
+ }
+}
+
+func (h *handler) FetchUserContributions(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ 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)
+}
diff --git a/backend/internal/app/contribution/service.go b/backend/internal/app/contribution/service.go
new file mode 100644
index 00000000..9a7e85c0
--- /dev/null
+++ b/backend/internal/app/contribution/service.go
@@ -0,0 +1,312 @@
+package contribution
+
+import (
+ "context"
+ "encoding/json"
+ "log/slog"
+ "net/http"
+
+ "github.com/joshsoftware/code-curiosity-2025/internal/app/bigquery"
+ repoService "github.com/joshsoftware/code-curiosity-2025/internal/app/repository"
+ "github.com/joshsoftware/code-curiosity-2025/internal/app/transaction"
+ "github.com/joshsoftware/code-curiosity-2025/internal/app/user"
+ "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors"
+ "github.com/joshsoftware/code-curiosity-2025/internal/repository"
+ "google.golang.org/api/iterator"
+)
+
+// github event names
+const (
+ pullRequestEvent = "PullRequestEvent"
+ issuesEvent = "IssuesEvent"
+ pushEvent = "PushEvent"
+ issueCommentEvent = "IssueCommentEvent"
+)
+
+// app contribution types
+const (
+ pullRequestMerged = "PullRequestMerged"
+ pullRequestOpened = "PullRequestOpened"
+ issueOpened = "IssueOpened"
+ issueClosed = "IssueClosed"
+ issueResolved = "IssueResolved"
+ pullRequestUpdated = "PullRequestUpdated"
+ issueComment = "IssueComment"
+ pullRequestComment = "PullRequestComment"
+)
+
+// payload
+const (
+ payloadActionKey = "action"
+ payloadPullRequestKey = "pull_request"
+ PayloadMergedKey = "merged"
+ PayloadIssueKey = "issue"
+ PayloadStateReasonKey = "state_reason"
+ PayloadClosedKey = "closed"
+ PayloadOpenedKey = "opened"
+ PayloadNotPlannedKey = "not_planned"
+ PayloadCompletedKey = "completed"
+)
+
+type service struct {
+ bigqueryService bigquery.Service
+ contributionRepository repository.ContributionRepository
+ repositoryService repoService.Service
+ userService user.Service
+ transactionService transaction.Service
+ httpClient *http.Client
+}
+
+type Service interface {
+ ProcessFetchedContributions(ctx context.Context) error
+ ProcessEachContribution(ctx context.Context, contribution ContributionResponse) error
+ GetContributionType(ctx context.Context, contribution ContributionResponse) (string, error)
+ CreateContribution(ctx context.Context, contributionType string, contributionDetails ContributionResponse, repositoryId int, userId int) (Contribution, error)
+ HandleContributionCreation(ctx context.Context, repositoryID int, contribution ContributionResponse) (Contribution, error)
+ GetContributionScoreDetailsByContributionType(ctx context.Context, contributionType string) (ContributionScore, error)
+ FetchUserContributions(ctx context.Context, userId int) ([]FetchUserContributionsResponse, error)
+ GetContributionByGithubEventId(ctx context.Context, githubEventId string) (Contribution, error)
+ ListMonthlyContributionSummary(ctx context.Context, year int, monthParam int, userId int) ([]MonthlyContributionSummary, error)
+}
+
+func NewService(bigqueryService bigquery.Service, contributionRepository repository.ContributionRepository, repositoryService repoService.Service, userService user.Service, transactionService transaction.Service, httpClient *http.Client) Service {
+ return &service{
+ bigqueryService: bigqueryService,
+ contributionRepository: contributionRepository,
+ repositoryService: repositoryService,
+ userService: userService,
+ transactionService: transactionService,
+ httpClient: httpClient,
+ }
+}
+
+func (s *service) ProcessFetchedContributions(ctx context.Context) error {
+ contributions, err := s.bigqueryService.FetchDailyContributions(ctx)
+ if err != nil {
+ slog.Error("error fetching daily contributions", "error", err)
+ return apperrors.ErrFetchingFromBigquery
+ }
+
+ //using a local copy here to copy contribution so that I can implement retry mechanism in future
+ //thinking of batch processing to be implemented later on, to handle memory overflow
+ var fetchedContributions []ContributionResponse
+
+ for {
+ var contribution ContributionResponse
+ err := contributions.Next(&contribution)
+ if err != nil {
+ if err == iterator.Done {
+ break
+ }
+
+ slog.Error("error iterating contribution rows", "error", err)
+ return apperrors.ErrNextContribution
+ }
+
+ fetchedContributions = append(fetchedContributions, contribution)
+ }
+
+ for _, contribution := range fetchedContributions {
+ err := s.ProcessEachContribution(ctx, contribution)
+ if err != nil {
+ slog.Error("error processing contribution with github event id", "github event id", "error", contribution.ID, err)
+ return err
+ }
+ }
+
+ return nil
+}
+
+func (s *service) ProcessEachContribution(ctx context.Context, contribution ContributionResponse) error {
+ obtainedContribution, err := s.GetContributionByGithubEventId(ctx, contribution.ID)
+ if err != nil {
+ if err == apperrors.ErrContributionNotFound {
+ obtainedRepository, err := s.repositoryService.HandleRepositoryCreation(ctx, repoService.ContributionResponse(contribution))
+ if err != nil {
+ slog.Error("error handling repository creation", "error", err)
+ return err
+ }
+ obtainedContribution, err = s.HandleContributionCreation(ctx, obtainedRepository.Id, contribution)
+ if err != nil {
+ slog.Error("error handling contribution creation", "error", err)
+ return err
+ }
+ } else {
+ slog.Error("error fetching contribution by github event id", "error", err)
+ return err
+ }
+ }
+
+ _, err = s.transactionService.HandleTransactionCreation(ctx, transaction.Contribution(obtainedContribution))
+ if err != nil {
+ slog.Error("error handling transaction creation", "error", err)
+ return err
+ }
+
+ return nil
+}
+
+func (s *service) GetContributionType(ctx context.Context, contribution ContributionResponse) (string, error) {
+ var contributionPayload map[string]interface{}
+ err := json.Unmarshal([]byte(contribution.Payload), &contributionPayload)
+ if err != nil {
+ slog.Warn("invalid payload", "error", err)
+ return "", err
+ }
+
+ var action string
+ if actionVal, ok := contributionPayload[payloadActionKey]; ok {
+ action = actionVal.(string)
+ }
+
+ var pullRequest map[string]interface{}
+ var isMerged bool
+ if pullRequestPayload, ok := contributionPayload[payloadPullRequestKey]; ok {
+ pullRequest = pullRequestPayload.(map[string]interface{})
+ isMerged = pullRequest[PayloadMergedKey].(bool)
+ }
+
+ var issue map[string]interface{}
+ var stateReason string
+ if issuePayload, ok := contributionPayload[PayloadIssueKey]; ok {
+ issue = issuePayload.(map[string]interface{})
+ stateReason = issue[PayloadStateReasonKey].(string)
+ }
+
+ var contributionType string
+ switch contribution.Type {
+ case pullRequestEvent:
+ if action == PayloadClosedKey && isMerged {
+ contributionType = pullRequestMerged
+ } else if action == PayloadOpenedKey {
+ contributionType = pullRequestOpened
+ }
+
+ case issuesEvent:
+ if action == PayloadOpenedKey {
+ contributionType = issueOpened
+ } else if action == PayloadClosedKey && stateReason == PayloadNotPlannedKey {
+ contributionType = issueClosed
+ } else if action == PayloadClosedKey && stateReason == PayloadCompletedKey {
+ contributionType = issueResolved
+ }
+
+ case pushEvent:
+ contributionType = pullRequestUpdated
+
+ case issueCommentEvent:
+ contributionType = issueComment
+
+ case pullRequestComment:
+ contributionType = pullRequestComment
+ }
+
+ return contributionType, nil
+}
+
+func (s *service) CreateContribution(ctx context.Context, contributionType string, contributionDetails ContributionResponse, repositoryId int, userId int) (Contribution, error) {
+ contribution := Contribution{
+ UserId: userId,
+ RepositoryId: repositoryId,
+ ContributionType: contributionType,
+ ContributedAt: contributionDetails.CreatedAt,
+ GithubEventId: contributionDetails.ID,
+ }
+
+ contributionScoreDetails, err := s.GetContributionScoreDetailsByContributionType(ctx, contributionType)
+ if err != nil {
+ slog.Error("error occured while getting contribution score details", "error", err)
+ return Contribution{}, err
+ }
+
+ contribution.ContributionScoreId = contributionScoreDetails.Id
+ contribution.BalanceChange = contributionScoreDetails.Score
+
+ contributionResponse, err := s.contributionRepository.CreateContribution(ctx, nil, repository.Contribution(contribution))
+ if err != nil {
+ slog.Error("error creating contribution", "error", err)
+ return Contribution{}, err
+ }
+
+ return Contribution(contributionResponse), nil
+}
+
+func (s *service) HandleContributionCreation(ctx context.Context, repositoryID int, contribution ContributionResponse) (Contribution, error) {
+ user, err := s.userService.GetUserByGithubId(ctx, contribution.ActorID)
+ if err != nil {
+ slog.Error("error getting user id", "error", err)
+ return Contribution{}, err
+ }
+
+ contributionType, err := s.GetContributionType(ctx, contribution)
+ if err != nil {
+ slog.Error("error getting contribution type", "error", err)
+ return Contribution{}, err
+ }
+
+ obtainedContribution, err := s.CreateContribution(ctx, contributionType, contribution, repositoryID, user.Id)
+ if err != nil {
+ slog.Error("error creating contribution", "error", err)
+ return Contribution{}, err
+ }
+
+ return obtainedContribution, nil
+}
+
+func (s *service) GetContributionScoreDetailsByContributionType(ctx context.Context, contributionType string) (ContributionScore, error) {
+ contributionScoreDetails, err := s.contributionRepository.GetContributionScoreDetailsByContributionType(ctx, nil, contributionType)
+ if err != nil {
+ slog.Error("error occured while getting contribution score details", "error", err)
+ return ContributionScore{}, err
+ }
+
+ return ContributionScore(contributionScoreDetails), nil
+}
+
+func (s *service) FetchUserContributions(ctx context.Context, 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
+}
diff --git a/backend/internal/app/cronJob/cleanupJob.go b/backend/internal/app/cronJob/cleanupJob.go
new file mode 100644
index 00000000..d87b3c99
--- /dev/null
+++ b/backend/internal/app/cronJob/cleanupJob.go
@@ -0,0 +1,32 @@
+package cronJob
+
+import (
+ "context"
+
+ "github.com/joshsoftware/code-curiosity-2025/internal/app/user"
+)
+
+type CleanupJob struct {
+ CronJob
+ userService user.Service
+}
+
+func NewCleanupJob(userService user.Service) *CleanupJob {
+ return &CleanupJob{
+ userService: userService,
+ CronJob: CronJob{Name: "User Cleanup Job Daily"},
+ }
+}
+
+func (c *CleanupJob) Schedule(s *CronSchedular) error {
+ _, err := s.cron.AddFunc("00 18 * * *", func() { c.Execute(context.Background(), c.run) })
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (c *CleanupJob) run(ctx context.Context) {
+ c.userService.HardDeleteUsers(ctx)
+}
diff --git a/backend/internal/app/cronJob/cronjob.go b/backend/internal/app/cronJob/cronjob.go
new file mode 100644
index 00000000..f8f3a219
--- /dev/null
+++ b/backend/internal/app/cronJob/cronjob.go
@@ -0,0 +1,24 @@
+package cronJob
+
+import (
+ "context"
+ "log/slog"
+ "time"
+)
+
+type Job interface {
+ Schedule(c *CronSchedular) error
+}
+
+type CronJob struct {
+ Name string
+}
+
+func (c *CronJob) Execute(ctx context.Context, fn func(context.Context)) {
+ slog.Info("cron job started at", "time ", time.Now())
+ defer func() {
+ slog.Info("cron job completed")
+ }()
+
+ fn(ctx)
+}
diff --git a/backend/internal/app/cronJob/dailyJob.go b/backend/internal/app/cronJob/dailyJob.go
new file mode 100644
index 00000000..67f3e24c
--- /dev/null
+++ b/backend/internal/app/cronJob/dailyJob.go
@@ -0,0 +1,32 @@
+package cronJob
+
+import (
+ "context"
+
+ "github.com/joshsoftware/code-curiosity-2025/internal/app/contribution"
+)
+
+type DailyJob struct {
+ CronJob
+ contributionService contribution.Service
+}
+
+func NewDailyJob(contributionService contribution.Service) *DailyJob {
+ return &DailyJob{
+ contributionService: contributionService,
+ CronJob: CronJob{Name: "Fetch Contributions Daily"},
+ }
+}
+
+func (d *DailyJob) Schedule(s *CronSchedular) error {
+ _, err := s.cron.AddFunc("0 1 * * *", func() { d.Execute(context.Background(), d.run) })
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (d *DailyJob) run(ctx context.Context) {
+ d.contributionService.ProcessFetchedContributions(ctx)
+}
diff --git a/backend/internal/app/cronJob/init.go b/backend/internal/app/cronJob/init.go
new file mode 100644
index 00000000..77d9f78b
--- /dev/null
+++ b/backend/internal/app/cronJob/init.go
@@ -0,0 +1,40 @@
+package cronJob
+
+import (
+ "log/slog"
+ "time"
+
+ "github.com/joshsoftware/code-curiosity-2025/internal/app/contribution"
+ "github.com/joshsoftware/code-curiosity-2025/internal/app/user"
+ "github.com/robfig/cron/v3"
+)
+
+type CronSchedular struct {
+ cron *cron.Cron
+}
+
+func NewCronSchedular() *CronSchedular {
+ location, err := time.LoadLocation("Asia/Kolkata")
+ if err != nil {
+ slog.Error("failed to load IST timezone", "error", err)
+ }
+
+ return &CronSchedular{
+ cron: cron.New(cron.WithLocation(location)),
+ }
+}
+
+func (c *CronSchedular) InitCronJobs(contributionService contribution.Service, userService user.Service) {
+ jobs := []Job{
+ NewDailyJob(contributionService),
+ NewCleanupJob(userService),
+ }
+
+ for _, job := range jobs {
+ if err := job.Schedule(c); err != nil {
+ slog.Error("failed to execute cron job")
+ }
+ }
+
+ c.cron.Start()
+}
diff --git a/backend/internal/app/dependencies.go b/backend/internal/app/dependencies.go
new file mode 100644
index 00000000..91510b77
--- /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, 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..1bc0916b
--- /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:"languagesUrl"`
+ UpdateDate time.Time `json:"updatedAt"`
+ RepoOwnerName RepoOwner `json:"owner"`
+ ContributorsUrl string `json:"contributorsUrl"`
+ RepoUrl string `json:"repoUrl"`
+}
+
+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:"avatarUrl"`
+ GithubUrl string `json:"githubUrl"`
+ Contributions int `json:"contributions"`
+}
diff --git a/backend/internal/app/github/service.go b/backend/internal/app/github/service.go
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..e822e84f
--- /dev/null
+++ b/backend/internal/app/goal/domain.go
@@ -0,0 +1,26 @@
+package goal
+
+import "time"
+
+type Goal struct {
+ Id int `json:"id"`
+ Level string `json:"level"`
+ CreatedAt time.Time `json:"createdAt"`
+ UpdatedAt time.Time `json:"updatedAt"`
+}
+
+type GoalContribution struct {
+ Id int `json:"id"`
+ GoalId int `json:"goalId"`
+ ContributionScoreId int `json:"contributionScoreId"`
+ TargetCount int `json:"targetCount"`
+ IsCustom bool `json:"isCustom"`
+ SetByUserId int `json:"setByUserId"`
+ CreatedAt time.Time `json:"createdAt"`
+ UpdatedAt time.Time `json:"updatedAt"`
+}
+
+type CustomGoalLevelTarget struct {
+ ContributionType string `json:"contributionType"`
+ Target int `json:"target"`
+}
diff --git a/backend/internal/app/goal/handler.go b/backend/internal/app/goal/handler.go
new file mode 100644
index 00000000..4fb12277
--- /dev/null
+++ b/backend/internal/app/goal/handler.go
@@ -0,0 +1,117 @@
+package goal
+
+import (
+ "encoding/json"
+ "log/slog"
+ "net/http"
+
+ "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors"
+ "github.com/joshsoftware/code-curiosity-2025/internal/pkg/middleware"
+ "github.com/joshsoftware/code-curiosity-2025/internal/pkg/response"
+)
+
+type handler struct {
+ goalService Service
+}
+
+type Handler interface {
+ ListGoalLevels(w http.ResponseWriter, r *http.Request)
+ ListGoalLevelTargets(w http.ResponseWriter, r *http.Request)
+ CreateCustomGoalLevelTarget(w http.ResponseWriter, r *http.Request)
+ ListGoalLevelAchievedTarget(w http.ResponseWriter, r *http.Request)
+}
+
+func NewHandler(goalService Service) Handler {
+ return &handler{
+ goalService: goalService,
+ }
+}
+
+func (h *handler) ListGoalLevels(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ gaols, err := h.goalService.ListGoalLevels(ctx)
+ if err != nil {
+ slog.Error("error fetching users conributed repos", "error", err)
+ status, errorMessage := apperrors.MapError(err)
+ response.WriteJson(w, status, errorMessage, nil)
+ return
+ }
+
+ response.WriteJson(w, http.StatusOK, "goal levels fetched successfully", gaols)
+}
+
+func (h *handler) ListGoalLevelTargets(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ userIdCtxVal := ctx.Value(middleware.UserIdKey)
+ userId, ok := userIdCtxVal.(int)
+ if !ok {
+ slog.Error("error obtaining user id from context")
+ status, errorMessage := apperrors.MapError(apperrors.ErrContextValue)
+ response.WriteJson(w, status, errorMessage, nil)
+ return
+ }
+
+ goalLevelTargets, err := h.goalService.ListGoalLevelTargetDetail(ctx, userId)
+ if err != nil {
+ slog.Error("error fetching goal level targets", "error", err)
+ status, errorMessage := apperrors.MapError(err)
+ response.WriteJson(w, status, errorMessage, nil)
+ return
+ }
+
+ response.WriteJson(w, http.StatusOK, "goal level targets fetched successfully", goalLevelTargets)
+}
+
+func (h *handler) CreateCustomGoalLevelTarget(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ userIdCtxVal := ctx.Value(middleware.UserIdKey)
+ userId, ok := userIdCtxVal.(int)
+ if !ok {
+ slog.Error("error obtaining user id from context")
+ status, errorMessage := apperrors.MapError(apperrors.ErrContextValue)
+ response.WriteJson(w, status, errorMessage, nil)
+ return
+ }
+
+ var customGoalLevelTarget []CustomGoalLevelTarget
+ err := json.NewDecoder(r.Body).Decode(&customGoalLevelTarget)
+ if err != nil {
+ slog.Error(apperrors.ErrFailedMarshal.Error(), "error", err)
+ response.WriteJson(w, http.StatusBadRequest, apperrors.ErrInvalidRequestBody.Error(), nil)
+ return
+ }
+
+ createdCustomGoalLevelTargets, err := h.goalService.CreateCustomGoalLevelTarget(ctx, userId, customGoalLevelTarget)
+ if err != nil {
+ slog.Error(apperrors.ErrFailedMarshal.Error(), "error", err)
+ response.WriteJson(w, http.StatusBadRequest, err.Error(), nil)
+ return
+ }
+
+ response.WriteJson(w, http.StatusOK, "custom goal level targets created successfully", createdCustomGoalLevelTargets)
+}
+
+func (h *handler) ListGoalLevelAchievedTarget(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ userIdCtxVal := ctx.Value(middleware.UserIdKey)
+ userId, ok := userIdCtxVal.(int)
+ if !ok {
+ slog.Error("error obtaining user id from context")
+ status, errorMessage := apperrors.MapError(apperrors.ErrContextValue)
+ response.WriteJson(w, status, errorMessage, nil)
+ return
+ }
+
+ goalLevelAchievedTarget, err := h.goalService.ListGoalLevelAchievedTarget(ctx, userId)
+ if err != nil {
+ slog.Error("error failed to list goal level achieved targets", "error", err)
+ response.WriteJson(w, http.StatusBadRequest, err.Error(), nil)
+ return
+ }
+
+ response.WriteJson(w, http.StatusOK, "goal level achieved targets fetched successfully", goalLevelAchievedTarget)
+}
diff --git a/backend/internal/app/goal/service.go b/backend/internal/app/goal/service.go
new file mode 100644
index 00000000..33a71f39
--- /dev/null
+++ b/backend/internal/app/goal/service.go
@@ -0,0 +1,164 @@
+package goal
+
+import (
+ "context"
+ "log/slog"
+ "time"
+
+ "github.com/joshsoftware/code-curiosity-2025/internal/app/badge"
+ "github.com/joshsoftware/code-curiosity-2025/internal/repository"
+)
+
+type service struct {
+ goalRepository repository.GoalRepository
+ contributionRepository repository.ContributionRepository
+ badgeService badge.Service
+}
+
+type Service interface {
+ ListGoalLevels(ctx context.Context) ([]Goal, error)
+ GetGoalIdByGoalLevel(ctx context.Context, level string) (int, error)
+ ListGoalLevelTargetDetail(ctx context.Context, userId int) ([]GoalContribution, error)
+ CreateCustomGoalLevelTarget(ctx context.Context, userId int, customGoalLevelTarget []CustomGoalLevelTarget) ([]GoalContribution, error)
+ ListGoalLevelAchievedTarget(ctx context.Context, userId int) (map[string]int, error)
+}
+
+func NewService(goalRepository repository.GoalRepository, contributionRepository repository.ContributionRepository, badgeService badge.Service) Service {
+ return &service{
+ goalRepository: goalRepository,
+ contributionRepository: contributionRepository,
+ badgeService: badgeService,
+ }
+}
+
+func (s *service) ListGoalLevels(ctx context.Context) ([]Goal, error) {
+ goals, err := s.goalRepository.ListGoalLevels(ctx, nil)
+ if err != nil {
+ slog.Error("error fetching goal levels", "error", err)
+ return nil, err
+ }
+
+ serviceGoals := make([]Goal, len(goals))
+
+ for i, g := range goals {
+ serviceGoals[i] = Goal(g)
+ }
+
+ return serviceGoals, nil
+}
+
+func (s *service) GetGoalIdByGoalLevel(ctx context.Context, level string) (int, error) {
+ goalId, err := s.goalRepository.GetGoalIdByGoalLevel(ctx, nil, level)
+
+ if err != nil {
+ slog.Error("failed to get goal id by goal level", "error", err)
+ return 0, err
+ }
+
+ return goalId, err
+}
+
+func (s *service) ListGoalLevelTargetDetail(ctx context.Context, userId int) ([]GoalContribution, error) {
+ goalLevelTargets, err := s.goalRepository.ListUserGoalLevelTargets(ctx, nil, userId)
+ if err != nil {
+ slog.Error("error fetching goal level targets", "error", err)
+ return nil, err
+ }
+
+ serviceGoalLevelTargets := make([]GoalContribution, len(goalLevelTargets))
+ for i, g := range goalLevelTargets {
+ serviceGoalLevelTargets[i] = GoalContribution(g)
+ }
+
+ return serviceGoalLevelTargets, nil
+}
+
+func (s *service) CreateCustomGoalLevelTarget(ctx context.Context, userId int, customGoalLevelTarget []CustomGoalLevelTarget) ([]GoalContribution, error) {
+ customGoalLevelId, err := s.GetGoalIdByGoalLevel(ctx, "Custom")
+ if err != nil {
+ slog.Error("error fetching custom goal level id", "error", err)
+ return nil, err
+ }
+ var goalContributions []GoalContribution
+
+ goalContributionInfo := make([]GoalContribution, len(customGoalLevelTarget))
+ for i, c := range customGoalLevelTarget {
+ goalContributionInfo[i].GoalId = customGoalLevelId
+
+ contributionScoreDetails, err := s.contributionRepository.GetContributionScoreDetailsByContributionType(ctx, nil, c.ContributionType)
+ if err != nil {
+ slog.Error("error fetching contribution score details by type", "error", err)
+ return nil, err
+ }
+
+ goalContributionInfo[i].ContributionScoreId = contributionScoreDetails.Id
+ goalContributionInfo[i].TargetCount = c.Target
+ goalContributionInfo[i].SetByUserId = userId
+
+ goalContribution, err := s.goalRepository.CreateCustomGoalLevelTarget(ctx, nil, repository.GoalContribution(goalContributionInfo[i]))
+ if err != nil {
+ slog.Error("error creating custom goal level target", "error", err)
+ return nil, err
+ }
+
+ goalContributions = append(goalContributions, GoalContribution(goalContribution))
+ }
+
+ return goalContributions, nil
+}
+
+func (s *service) ListGoalLevelAchievedTarget(ctx context.Context, userId int) (map[string]int, error) {
+ goalLevelSetTargets, err := s.goalRepository.ListUserGoalLevelTargets(ctx, nil, userId)
+ if err != nil {
+ slog.Error("error fetching goal level targets", "error", err)
+ return nil, err
+ }
+
+ contributionTypes := make([]CustomGoalLevelTarget, len(goalLevelSetTargets))
+ for i, g := range goalLevelSetTargets {
+ contributionTypes[i].ContributionType, err = s.contributionRepository.GetContributionTypeByContributionScoreId(ctx, nil, g.ContributionScoreId)
+ if err != nil {
+ slog.Error("error fetching contribution type by contribution score id", "error", err)
+ return nil, err
+ }
+
+ contributionTypes[i].Target = g.TargetCount
+ }
+
+ year := int(time.Now().Year())
+ month := int(time.Now().Month())
+ monthlyContributionCount, err := s.contributionRepository.ListMonthlyContributionSummary(ctx, nil, year, month, userId)
+ if err != nil {
+ slog.Error("error fetching monthly contribution count", "error", err)
+ return nil, err
+ }
+
+ contributionsAchievedTarget := make(map[string]int, len(monthlyContributionCount))
+
+ for _, m := range monthlyContributionCount {
+ contributionsAchievedTarget[m.Type] = m.Count
+ }
+
+ var completedTarget int
+ for _, c := range contributionTypes {
+ if c.Target == contributionsAchievedTarget[c.ContributionType] {
+ completedTarget += 1
+ }
+ }
+
+ if completedTarget == len(goalLevelSetTargets) {
+ userGoalLevel, err := s.goalRepository.GetUserActiveGoalLevel(ctx, nil, userId)
+ if err != nil {
+ slog.Error("error fetching user active gaol level", "error", err)
+ return nil, err
+ }
+
+ _, err = s.badgeService.HandleBadgeCreation(ctx, userId, userGoalLevel)
+ if err != nil {
+ slog.Error("error handling user badge creation", "error", err)
+ return nil, err
+ }
+ }
+
+ return contributionsAchievedTarget, nil
+}
diff --git a/backend/internal/app/repository/domain.go b/backend/internal/app/repository/domain.go
new file mode 100644
index 00000000..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..13ce566a
--- /dev/null
+++ b/backend/internal/app/router.go
@@ -0,0 +1,45 @@
+package app
+
+import (
+ "net/http"
+
+ "github.com/joshsoftware/code-curiosity-2025/internal/pkg/middleware"
+ "github.com/joshsoftware/code-curiosity-2025/internal/pkg/response"
+)
+
+func NewRouter(deps Dependencies) http.Handler {
+ router := http.NewServeMux()
+
+ router.HandleFunc("GET /api/v1/health", func(w http.ResponseWriter, r *http.Request) {
+ response.WriteJson(w, http.StatusOK, "Server is up and running..", nil)
+ })
+
+ router.HandleFunc("GET /api/v1/auth/github", deps.AuthHandler.GithubOAuthLoginUrl)
+ router.HandleFunc("GET /api/v1/auth/github/callback", deps.AuthHandler.GithubOAuthLoginCallback)
+ router.HandleFunc("GET /api/v1/auth/user", middleware.Authentication(deps.AuthHandler.GetLoggedInUser, deps.AppCfg))
+
+ router.HandleFunc("PATCH /api/v1/user/email", middleware.Authentication(deps.UserHandler.UpdateUserEmail, deps.AppCfg))
+ router.HandleFunc("DELETE /api/v1/user/delete/{user_id}", middleware.Authentication(deps.UserHandler.SoftDeleteUser, deps.AppCfg))
+
+ router.HandleFunc("GET /api/v1/user/contributions/all", middleware.Authentication(deps.ContributionHandler.FetchUserContributions, deps.AppCfg))
+ router.HandleFunc("GET /api/v1/user/overview", middleware.Authentication(deps.ContributionHandler.ListMonthlyContributionSummary, deps.AppCfg))
+
+ router.HandleFunc("GET /api/v1/user/repositories", middleware.Authentication(deps.RepositoryHandler.FetchUsersContributedRepos, deps.AppCfg))
+ router.HandleFunc("GET /api/v1/user/repositories/{repo_id}", middleware.Authentication(deps.RepositoryHandler.FetchParticularRepoDetails, deps.AppCfg))
+ router.HandleFunc("GET /api/v1/user/repositories/contributions/recent/{repo_id}", middleware.Authentication(deps.RepositoryHandler.FetchUserContributionsInRepo, deps.AppCfg))
+ router.HandleFunc("GET /api/v1/user/repositories/languages/{repo_id}", middleware.Authentication(deps.RepositoryHandler.FetchLanguagePercentInRepo, deps.AppCfg))
+ router.HandleFunc("GET /api/v1/user/repositories/contributors/{repo_id}", middleware.Authentication(deps.RepositoryHandler.FetchParticularRepoContributors, deps.AppCfg))
+
+ router.HandleFunc("GET /api/v1/leaderboard", middleware.Authentication(deps.UserHandler.ListUserRanks, deps.AppCfg))
+ router.HandleFunc("GET /api/v1/user/leaderboard", middleware.Authentication(deps.UserHandler.GetCurrentUserRank, deps.AppCfg))
+
+ router.HandleFunc("GET /api/v1/user/goal/level", middleware.Authentication(deps.GoalHandler.ListGoalLevels, deps.AppCfg))
+ router.HandleFunc("PATCH /api/v1/user/goal/level", middleware.Authentication(deps.UserHandler.UpdateCurrentActiveGoalId, deps.AppCfg))
+ router.HandleFunc("GET /api/v1/user/goal/level/targets", middleware.Authentication(deps.GoalHandler.ListGoalLevelTargets, deps.AppCfg))
+ router.HandleFunc("POST /api/v1/user/goal/level/custom/targets", middleware.Authentication(deps.GoalHandler.CreateCustomGoalLevelTarget, deps.AppCfg))
+ router.HandleFunc("GET /api/v1/user/goal/level/targets/achieved", middleware.Authentication(deps.GoalHandler.ListGoalLevelAchievedTarget, deps.AppCfg))
+
+ router.HandleFunc("GET /api/v1/user/badges", middleware.Authentication(deps.BadgeHandler.GetBadgeDetailsOfUser, deps.AppCfg))
+
+ return middleware.CorsMiddleware(router, deps.AppCfg)
+}
diff --git a/backend/internal/app/transaction/domain.go b/backend/internal/app/transaction/domain.go
new file mode 100644
index 00000000..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..4599fe80
--- /dev/null
+++ b/backend/internal/app/user/domain.go
@@ -0,0 +1,60 @@
+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"`
+}
+
+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"`
+}
diff --git a/backend/internal/app/user/handler.go b/backend/internal/app/user/handler.go
new file mode 100644
index 00000000..8133c9f9
--- /dev/null
+++ b/backend/internal/app/user/handler.go
@@ -0,0 +1,151 @@
+package user
+
+import (
+ "encoding/json"
+ "log/slog"
+ "net/http"
+
+ "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors"
+ "github.com/joshsoftware/code-curiosity-2025/internal/pkg/middleware"
+ "github.com/joshsoftware/code-curiosity-2025/internal/pkg/response"
+)
+
+type handler struct {
+ userService Service
+}
+
+type Handler interface {
+ UpdateUserEmail(w http.ResponseWriter, r *http.Request)
+ SoftDeleteUser(w http.ResponseWriter, r *http.Request)
+ ListUserRanks(w http.ResponseWriter, r *http.Request)
+ GetCurrentUserRank(w http.ResponseWriter, r *http.Request)
+ UpdateCurrentActiveGoalId(w http.ResponseWriter, r *http.Request)
+}
+
+func NewHandler(userService Service) Handler {
+ return &handler{
+ userService: userService,
+ }
+}
+
+func (h *handler) UpdateUserEmail(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ 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
+ }
+
+ currentUserRank, err := h.userService.GetCurrentUserRank(ctx, userId)
+ if err != nil {
+ slog.Error("failed to get current user rank", "error", err)
+ status, errorMessage := apperrors.MapError(err)
+ response.WriteJson(w, status, errorMessage, nil)
+ return
+ }
+
+ response.WriteJson(w, http.StatusOK, "current user rank fetched successfully", currentUserRank)
+}
+
+func (h *handler) UpdateCurrentActiveGoalId(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ userIdCtxVal := ctx.Value(middleware.UserIdKey)
+ userId, ok := userIdCtxVal.(int)
+ if !ok {
+ slog.Error("error obtaining user id from context")
+ status, errorMessage := apperrors.MapError(apperrors.ErrContextValue)
+ response.WriteJson(w, status, errorMessage, nil)
+ return
+ }
+
+ var goal GoalLevel
+ err := json.NewDecoder(r.Body).Decode(&goal)
+ if err != nil {
+ slog.Error(apperrors.ErrFailedMarshal.Error(), "error", err)
+ response.WriteJson(w, http.StatusBadRequest, apperrors.ErrInvalidRequestBody.Error(), nil)
+ return
+ }
+
+ goalId, err := h.userService.UpdateCurrentActiveGoalId(ctx, userId, goal.Level)
+ if err != nil {
+ slog.Error("failed to update current active goal id", "error", err)
+ status, errMsg := apperrors.MapError(err)
+ response.WriteJson(w, status, errMsg, nil)
+ return
+ }
+
+ response.WriteJson(w, http.StatusOK, "Goal updated successfully", goalId)
+}
diff --git a/backend/internal/app/user/service.go b/backend/internal/app/user/service.go
new file mode 100644
index 00000000..2e58ba79
--- /dev/null
+++ b/backend/internal/app/user/service.go
@@ -0,0 +1,202 @@
+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)
+}
+
+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
+}
diff --git a/internal/config/app.go b/backend/internal/config/app.go
similarity index 65%
rename from internal/config/app.go
rename to backend/internal/config/app.go
index c4715f5c..4070c0f9 100644
--- a/internal/config/app.go
+++ b/backend/internal/config/app.go
@@ -25,13 +25,19 @@ type GithubOauth struct {
RedirectURL string `yaml:"redirect_url" required:"true"`
}
+type BigqueryProject struct {
+ ProjectID string `yaml:"project_id" required:"true"`
+}
+
type AppConfig struct {
- IsProduction bool `yaml:"is_production"`
- HTTPServer HTTPServer `yaml:"http_server"`
- Database Database `yaml:"database"`
- JWTSecret string `yaml:"jwt_secret"`
- ClientURL string `yaml:"client_url"`
- GithubOauth GithubOauth `yaml:"github_oauth"`
+ IsProduction bool `yaml:"is_production"`
+ HTTPServer HTTPServer `yaml:"http_server"`
+ Database Database `yaml:"database"`
+ JWTSecret string `yaml:"jwt_secret"`
+ ClientURL string `yaml:"client_url"`
+ GithubOauth GithubOauth `yaml:"github_oauth"`
+ BigqueryProject BigqueryProject `yaml:"bigquery_project"`
+ GithubPersonalAccessToken string `yaml:"github_personal_access_token"`
}
func LoadAppConfig() (AppConfig, error) {
diff --git a/backend/internal/config/bigquery.go b/backend/internal/config/bigquery.go
new file mode 100644
index 00000000..30294c5d
--- /dev/null
+++ b/backend/internal/config/bigquery.go
@@ -0,0 +1,23 @@
+package config
+
+import (
+ "context"
+
+ "cloud.google.com/go/bigquery"
+)
+
+type Bigquery struct {
+ Client *bigquery.Client
+}
+
+func BigqueryInit(ctx context.Context, appCfg AppConfig) (Bigquery, error) {
+ client, err := bigquery.NewClient(ctx, appCfg.BigqueryProject.ProjectID)
+ if err != nil {
+ return Bigquery{}, err
+ }
+
+ bigqueryInstance := Bigquery{
+ Client: client,
+ }
+ return bigqueryInstance, nil
+}
diff --git a/internal/config/db.go b/backend/internal/config/db.go
similarity index 100%
rename from internal/config/db.go
rename to backend/internal/config/db.go
diff --git a/internal/db/migrate.go b/backend/internal/db/migrate.go
similarity index 94%
rename from internal/db/migrate.go
rename to backend/internal/db/migrate.go
index 634e51a7..6b9a159f 100644
--- a/internal/db/migrate.go
+++ b/backend/internal/db/migrate.go
@@ -42,7 +42,7 @@ func InitMainDBMigrations(config config.AppConfig) (migration Migration, er erro
return
}
-func (migration Migration) MigrationsUpAll(){
+func (migration Migration) MigrationsUpAll() {
err := migration.m.Up()
if err != nil {
if err == migrate.ErrNoChange {
@@ -56,7 +56,7 @@ func (migration Migration) MigrationsUpAll(){
slog.Info("Migration up completed")
}
-func (migration Migration) MigrationsUpWithSteps(steps int){
+func (migration Migration) MigrationsUpWithSteps(steps int) {
if err := migration.m.Steps(steps); err != nil {
if err == migrate.ErrNoChange {
slog.Error("No new migrations to apply")
@@ -65,7 +65,7 @@ func (migration Migration) MigrationsUpWithSteps(steps int){
slog.Error("An error occurred while making migrations up", "error", err)
return
- }
+ }
slog.Info("Current migration version:", "version", migration.MigrationVersion())
slog.Info("Migration up completed")
@@ -110,7 +110,7 @@ func (migration Migration) MigrationsDownWithSteps(steps int) {
slog.Error("An error occurred while making migrations down", "error", err)
return
- }
+ }
slog.Info("Current migration version:", "version", migration.MigrationVersion())
slog.Info("Migration down completed")
@@ -206,13 +206,17 @@ func main() {
}
action := os.Args[1]
+ var steps string
+ if len(os.Args) > 2 {
+ steps = os.Args[2]
+ }
switch action {
case "up":
- migration.MigrationsUp(os.Args[2])
+ migration.MigrationsUp(steps)
case "down":
- migration.MigrationsDown(os.Args[2])
+ migration.MigrationsDown(steps)
case "create":
- migration.CreateMigrationFile(os.Args[2])
+ migration.CreateMigrationFile(steps)
default:
slog.Info("Invalid action. Use 'up' or 'down' or 'create'.")
}
diff --git a/internal/db/migrations/1748862201_init.down.sql b/backend/internal/db/migrations/1748862201_init.down.sql
similarity index 100%
rename from internal/db/migrations/1748862201_init.down.sql
rename to backend/internal/db/migrations/1748862201_init.down.sql
diff --git a/internal/db/migrations/1748862201_init.up.sql b/backend/internal/db/migrations/1748862201_init.up.sql
similarity index 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/pkg/apperrors/errors.go b/backend/internal/pkg/apperrors/errors.go
new file mode 100644
index 00000000..c1e60415
--- /dev/null
+++ b/backend/internal/pkg/apperrors/errors.go
@@ -0,0 +1,78 @@
+package apperrors
+
+import (
+ "errors"
+ "net/http"
+)
+
+var (
+ ErrContextValue = errors.New("error obtaining value from context")
+ ErrInternalServer = errors.New("internal server error")
+
+ ErrInvalidRequestBody = errors.New("invalid or missing parameters in the request body")
+ ErrInvalidQueryParams = errors.New("invalid or missing query parameters")
+ ErrFailedMarshal = errors.New("failed to parse request body")
+
+ ErrUnauthorizedAccess = errors.New("unauthorized. please provide a valid access token")
+ ErrAccessForbidden = errors.New("access forbidden")
+ ErrInvalidToken = errors.New("invalid or expired token")
+
+ ErrFailedInitializingLogger = errors.New("failed to initialize logger")
+ ErrNoAppConfigPath = errors.New("no config path provided")
+ ErrFailedToLoadAppConfig = errors.New("failed to load environment configuration")
+
+ ErrLoginWithGithubFailed = errors.New("failed to login with Github")
+ ErrGithubTokenExchangeFailed = errors.New("failed to exchange Github token")
+ ErrFailedToGetGithubUser = errors.New("failed to get Github user info")
+ ErrFailedToGetUserEmail = errors.New("failed to get user email from Github")
+
+ ErrUserNotFound = errors.New("user not found")
+ ErrUserCreationFailed = errors.New("failed to create user")
+
+ ErrJWTCreationFailed = errors.New("failed to create jwt token")
+ ErrAuthorizationFailed = errors.New("failed to authorize user")
+
+ ErrRepoNotFound = errors.New("repository not found")
+ ErrRepoCreationFailed = errors.New("failed to create repo for user")
+ ErrCalculatingUserRepoTotalCoins = errors.New("error calculating total coins earned by user for the repository")
+ ErrFetchingUsersContributedRepos = errors.New("error fetching users contributed repositories")
+ ErrFetchingUserContributionsInRepo = errors.New("error fetching users contribution in repository")
+ 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")
+
+ ErrTransactionCreationFailed = errors.New("error failed to create transaction")
+ ErrTransactionNotFound = errors.New("error transaction for the contribution id does not exist")
+
+ ErrFetchingGoals = errors.New("error fetching goal levels ")
+ ErrGoalNotFound = errors.New("goal not found")
+ ErrCustomGoalTargetCreationFailed = errors.New("failed to create targets for custom goal level")
+
+ ErrBadgeCreationFailed = errors.New("failed to create badge for user")
+)
+
+func MapError(err error) (statusCode int, errMessage string) {
+ switch err {
+ case ErrInvalidRequestBody, ErrInvalidQueryParams, ErrContextValue:
+ return http.StatusBadRequest, err.Error()
+ case ErrUnauthorizedAccess:
+ return http.StatusUnauthorized, err.Error()
+ case ErrAccessForbidden:
+ return http.StatusForbidden, err.Error()
+ case ErrUserNotFound, ErrRepoNotFound, ErrContributionNotFound, ErrGoalNotFound:
+ return http.StatusNotFound, err.Error()
+ case ErrInvalidToken:
+ return http.StatusUnprocessableEntity, err.Error()
+ default:
+ return http.StatusInternalServerError, ErrInternalServer.Error()
+ }
+}
diff --git a/internal/pkg/jwt/jwt.go b/backend/internal/pkg/jwt/jwt.go
similarity index 100%
rename from internal/pkg/jwt/jwt.go
rename to backend/internal/pkg/jwt/jwt.go
diff --git a/internal/pkg/middleware/middleware.go b/backend/internal/pkg/middleware/middleware.go
similarity index 84%
rename from internal/pkg/middleware/middleware.go
rename to backend/internal/pkg/middleware/middleware.go
index 3ece0897..4c407894 100644
--- a/internal/pkg/middleware/middleware.go
+++ b/backend/internal/pkg/middleware/middleware.go
@@ -5,12 +5,17 @@ import (
"net/http"
"strings"
+ "github.com/jmoiron/sqlx"
"github.com/joshsoftware/code-curiosity-2025/internal/config"
"github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors"
"github.com/joshsoftware/code-curiosity-2025/internal/pkg/jwt"
"github.com/joshsoftware/code-curiosity-2025/internal/pkg/response"
)
+type txKeyType struct{}
+
+var txKey = txKeyType{}
+
type contextKey string
const (
@@ -18,6 +23,15 @@ const (
IsAdminKey contextKey = "isAdmin"
)
+func EmbedTxInContext(ctx context.Context, tx *sqlx.Tx) context.Context {
+ return context.WithValue(ctx, txKey, tx)
+}
+
+func ExtractTxFromContext(ctx context.Context) (*sqlx.Tx, bool) {
+ tx, ok := ctx.Value(txKey).(*sqlx.Tx)
+ return tx, ok
+}
+
func CorsMiddleware(next http.Handler, appCfg config.AppConfig) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", appCfg.ClientURL)
diff --git a/internal/pkg/response/response.go b/backend/internal/pkg/response/response.go
similarity index 100%
rename from internal/pkg/response/response.go
rename to backend/internal/pkg/response/response.go
diff --git a/backend/internal/pkg/utils/helper.go b/backend/internal/pkg/utils/helper.go
new file mode 100644
index 00000000..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..3298c5e6
--- /dev/null
+++ b/backend/internal/repository/badge.go
@@ -0,0 +1,86 @@
+package repository
+
+import (
+ "context"
+ "log/slog"
+ "time"
+
+ "github.com/jmoiron/sqlx"
+ "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors"
+)
+
+type badgeRepository struct {
+ BaseRepository
+}
+
+type BadgeRepository interface {
+ RepositoryTransaction
+ GetUserCurrentMonthBadge(ctx context.Context, tx *sqlx.Tx, userId int) (Badge, error)
+ CreateBadge(ctx context.Context, tx *sqlx.Tx, userId int, badgeType string) (Badge, error)
+ GetBadgeDetailsOfUser(ctx context.Context, tx *sqlx.Tx, userId int) ([]Badge, error)
+}
+
+func NewBadgeRepository(db *sqlx.DB) BadgeRepository {
+ return &badgeRepository{
+ BaseRepository: BaseRepository{db},
+ }
+}
+
+const (
+ createBadgeQuery = `
+ INSERT INTO badges(
+ user_id,
+ badge_type,
+ earned_at
+ )
+ VALUES($1, $2, $3)
+ RETURNING *`
+
+ getBadgeDetailsOfUserQuery = "SELECT * FROM badges WHERE user_id = $1 ORDER BY earned_at DESC"
+
+ getUserCurrentMonthBadgeQuery = `
+ SELECT * FROM badges
+ WHERE user_id = $1
+ AND earned_at >= DATE_TRUNC('month', CURRENT_DATE)
+ AND earned_at < DATE_TRUNC('month', CURRENT_DATE + INTERVAL '1 month')`
+)
+
+func (br *badgeRepository) GetUserCurrentMonthBadge(ctx context.Context, tx *sqlx.Tx, userId int) (Badge, error) {
+ executer := br.BaseRepository.initiateQueryExecuter(tx)
+
+ var badge Badge
+ err := executer.GetContext(ctx, &badge, getUserCurrentMonthBadgeQuery, userId)
+ if err != nil {
+ slog.Error("error fetching current month earned badge for user", "error", err)
+ return Badge{}, apperrors.ErrBadgeCreationFailed
+ }
+
+ return badge, nil
+
+}
+
+func (br *badgeRepository) CreateBadge(ctx context.Context, tx *sqlx.Tx, userId int, badgeType string) (Badge, error) {
+ executer := br.BaseRepository.initiateQueryExecuter(tx)
+
+ var createdBadge Badge
+ err := executer.GetContext(ctx, &createdBadge, createBadgeQuery, userId, badgeType, time.Now())
+ if err != nil {
+ slog.Error("error creating badge for user", "error", err)
+ return Badge{}, apperrors.ErrBadgeCreationFailed
+ }
+
+ return createdBadge, nil
+}
+
+func (br *badgeRepository) GetBadgeDetailsOfUser(ctx context.Context, tx *sqlx.Tx, userId int) ([]Badge, error) {
+ executer := br.BaseRepository.initiateQueryExecuter(tx)
+
+ var badges []Badge
+
+ err := executer.SelectContext(ctx, &badges, getBadgeDetailsOfUserQuery, userId)
+ if err != nil {
+ return nil, err
+ }
+
+ return badges, nil
+}
diff --git a/internal/repository/base.go b/backend/internal/repository/base.go
similarity index 88%
rename from internal/repository/base.go
rename to backend/internal/repository/base.go
index 5516997b..e4b0795e 100644
--- a/internal/repository/base.go
+++ b/backend/internal/repository/base.go
@@ -23,6 +23,9 @@ type RepositoryTransaction interface {
type QueryExecuter interface {
QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row
ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)
+ QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error)
+ GetContext(ctx context.Context, dest interface{}, query string, args ...interface{}) error
+ SelectContext(ctx context.Context, dest interface{}, query string, args ...interface{}) error
}
func (b *BaseRepository) BeginTx(ctx context.Context) (*sqlx.Tx, error) {
@@ -61,7 +64,7 @@ func (b *BaseRepository) HandleTransaction(ctx context.Context, tx *sqlx.Tx, inc
}
return nil
}
-
+
err := tx.Commit()
if err != nil {
slog.Error("error occurred while committing database transaction", "error", err)
diff --git a/backend/internal/repository/contribution.go b/backend/internal/repository/contribution.go
new file mode 100644
index 00000000..d3618464
--- /dev/null
+++ b/backend/internal/repository/contribution.go
@@ -0,0 +1,178 @@
+package repository
+
+import (
+ "context"
+ "database/sql"
+ "errors"
+ "log/slog"
+
+ "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)
+}
+
+func NewContributionRepository(db *sqlx.DB) ContributionRepository {
+ return &contributionRepository{
+ BaseRepository: BaseRepository{db},
+ }
+}
+
+const (
+ createContributionQuery = `
+ INSERT INTO contributions (
+ user_id,
+ repository_id,
+ contribution_score_id,
+ contribution_type,
+ balance_change,
+ contributed_at,
+ github_event_id
+ )
+ VALUES ($1, $2, $3, $4, $5, $6, $7)
+ RETURNING *`
+
+ getContributionScoreDetailsByContributionTypeQuery = `SELECT * from contribution_score where contribution_type=$1`
+
+ fetchUserContributionsQuery = `SELECT * from contributions where user_id=$1 order by contributed_at desc`
+
+ getContributionByGithubEventIdQuery = `SELECT * from contributions where github_event_id=$1`
+
+ getAllContributionTypesQuery = `SELECT * from contribution_score`
+
+ getMonthlyContributionSummaryQuery = `
+ SELECT
+ DATE_TRUNC('month', contributed_at) AS month,
+ contribution_type,
+ COUNT(*) AS contribution_count,
+ SUM(balance_change) AS total_coins
+ FROM contributions
+ WHERE user_id = $1
+ AND DATE_TRUNC('month', contributed_at) = MAKE_DATE($2, $3, 1)::timestamptz
+ GROUP BY
+ month, contribution_type;`
+
+ getContributionTypeByContributionScoreIdQuery = `SELECT contribution_type from contribution_score where id=$1`
+)
+
+func (cr *contributionRepository) CreateContribution(ctx context.Context, tx *sqlx.Tx, contributionInfo Contribution) (Contribution, error) {
+ executer := cr.BaseRepository.initiateQueryExecuter(tx)
+
+ var contribution Contribution
+ err := executer.GetContext(ctx, &contribution, createContributionQuery,
+ contributionInfo.UserId,
+ contributionInfo.RepositoryId,
+ contributionInfo.ContributionScoreId,
+ contributionInfo.ContributionType,
+ contributionInfo.BalanceChange,
+ contributionInfo.ContributedAt,
+ contributionInfo.GithubEventId,
+ )
+ if err != nil {
+ slog.Error("error occured while inserting contributions", "error", err)
+ return Contribution{}, apperrors.ErrContributionCreationFailed
+ }
+
+ return contribution, err
+}
+
+func (cr *contributionRepository) GetContributionScoreDetailsByContributionType(ctx context.Context, tx *sqlx.Tx, contributionType string) (ContributionScore, error) {
+ executer := cr.BaseRepository.initiateQueryExecuter(tx)
+
+ var contributionScoreDetails ContributionScore
+ err := executer.GetContext(ctx, &contributionScoreDetails, getContributionScoreDetailsByContributionTypeQuery, contributionType)
+ if err != nil {
+ if errors.Is(err, sql.ErrNoRows) {
+ slog.Warn("no contribution score details found for contribution type", "contributionType", contributionType)
+ return ContributionScore{}, apperrors.ErrContributionScoreNotFound
+ }
+
+ slog.Error("error occured while getting contribution score details", "error", err)
+ return ContributionScore{}, err
+ }
+
+ return contributionScoreDetails, nil
+}
+
+func (cr *contributionRepository) FetchUserContributions(ctx context.Context, tx *sqlx.Tx, 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
+}
diff --git a/backend/internal/repository/domain.go b/backend/internal/repository/domain.go
new file mode 100644
index 00000000..c3faf145
--- /dev/null
+++ b/backend/internal/repository/domain.go
@@ -0,0 +1,121 @@
+package repository
+
+import (
+ "database/sql"
+ "time"
+)
+
+type User struct {
+ Id int `db:"id"`
+ GithubId int `db:"github_id"`
+ GithubUsername string `db:"github_username"`
+ Email string `db:"email"`
+ AvatarUrl string `db:"avatar_url"`
+ CurrentBalance int `db:"current_balance"`
+ CurrentActiveGoalId sql.NullInt64 `db:"current_active_goal_id"`
+ IsBlocked bool `db:"is_blocked"`
+ IsAdmin bool `db:"is_admin"`
+ Password string `db:"password"`
+ IsDeleted bool `db:"is_deleted"`
+ DeletedAt sql.NullTime `db:"deleted_at"`
+ CreatedAt time.Time `db:"created_at"`
+ UpdatedAt time.Time `db:"updated_at"`
+}
+
+type CreateUserRequestBody struct {
+ GithubId int `db:"github_id"`
+ GithubUsername string `db:"github_username"`
+ AvatarUrl string `db:"avatar_url"`
+ Email string `db:"email"`
+ IsAdmin bool `db:"is_admin"`
+}
+
+type Contribution struct {
+ Id int `db:"id"`
+ UserId int `db:"user_id"`
+ RepositoryId int `db:"repository_id"`
+ ContributionScoreId int `db:"contribution_score_id"`
+ ContributionType string `db:"contribution_type"`
+ BalanceChange int `db:"balance_change"`
+ ContributedAt time.Time `db:"contributed_at"`
+ GithubEventId string `db:"github_event_id"`
+ CreatedAt time.Time `db:"created_at"`
+ UpdatedAt time.Time `db:"updated_at"`
+}
+
+type Repository struct {
+ Id int `db:"id"`
+ GithubRepoId int `db:"github_repo_id"`
+ RepoName string `db:"repo_name"`
+ Description string `db:"description"`
+ LanguagesUrl string `db:"languages_url"`
+ RepoUrl string `db:"repo_url"`
+ OwnerName string `db:"owner_name"`
+ UpdateDate time.Time `db:"update_date"`
+ ContributorsUrl string `db:"contributors_url"`
+ CreatedAt time.Time `db:"created_at"`
+ UpdatedAt time.Time `db:"updated_at"`
+}
+
+type ContributionScore struct {
+ Id int `db:"id"`
+ AdminId int `db:"admin_id"`
+ ContributionType string `db:"contribution_type"`
+ Score int `db:"score"`
+ CreatedAt time.Time `db:"created_at"`
+ UpdatedAt time.Time `db:"updated_at"`
+}
+
+type Transaction struct {
+ Id int `db:"id"`
+ UserId int `db:"user_id"`
+ ContributionId int `db:"contribution_id"`
+ IsRedeemed bool `db:"is_redeemed"`
+ IsGained bool `db:"is_gained"`
+ TransactedBalance int `db:"transacted_balance"`
+ TransactedAt time.Time `db:"transacted_at"`
+ CreatedAt time.Time `db:"created_at"`
+ UpdatedAt time.Time `db:"updated_at"`
+}
+
+type LeaderboardUser struct {
+ Id int `db:"id"`
+ GithubUsername string `db:"github_username"`
+ AvatarUrl string `db:"avatar_url"`
+ CurrentBalance int `db:"current_balance"`
+ Rank int `db:"rank"`
+}
+
+type MonthlyContributionSummary struct {
+ Type string `db:"contribution_type"`
+ Count int `db:"contribution_count"`
+ TotalCoins int `db:"total_coins"`
+ Month time.Time `db:"month"`
+}
+
+type Goal struct {
+ Id int `db:"id"`
+ Level string `db:"level"`
+ CreatedAt time.Time `db:"created_at"`
+ UpdatedAt time.Time `db:"updated_at"`
+}
+
+type GoalContribution struct {
+ Id int `db:"id"`
+ GoalId int `db:"goal_id"`
+ ContributionScoreId int `db:"contribution_score_id"`
+ TargetCount int `db:"target_count"`
+ IsCustom bool `db:"is_custom"`
+ SetByUserId int `db:"set_by_user_id"`
+ CreatedAt time.Time `db:"created_at"`
+ UpdatedAt time.Time `db:"updated_at"`
+}
+
+type Badge struct {
+ Id int `db:"id"`
+ UserId int `db:"user_id"`
+ BadgeType string `db:"badge_type"`
+ EarnedAt time.Time `db:"earned_at"`
+ CreatedAt time.Time `db:"created_at"`
+ UpdatedAt time.Time `db:"updated_at"`
+}
diff --git a/backend/internal/repository/goal.go b/backend/internal/repository/goal.go
new file mode 100644
index 00000000..f56bc5e2
--- /dev/null
+++ b/backend/internal/repository/goal.go
@@ -0,0 +1,139 @@
+package repository
+
+import (
+ "context"
+ "database/sql"
+ "errors"
+ "log/slog"
+
+ "github.com/jmoiron/sqlx"
+ "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors"
+)
+
+type goalRepository struct {
+ BaseRepository
+}
+
+type GoalRepository interface {
+ RepositoryTransaction
+ ListGoalLevels(ctx context.Context, tx *sqlx.Tx) ([]Goal, error)
+ GetGoalIdByGoalLevel(ctx context.Context, tx *sqlx.Tx, level string) (int, error)
+ ListUserGoalLevelTargets(ctx context.Context, tx *sqlx.Tx, userId int) ([]GoalContribution, error)
+ CreateCustomGoalLevelTarget(ctx context.Context, tx *sqlx.Tx, customGoalContributionInfo GoalContribution) (GoalContribution, error)
+ GetUserActiveGoalLevel(ctx context.Context, tx *sqlx.Tx, userId int) (string, error)
+}
+
+func NewGoalRepository(db *sqlx.DB) GoalRepository {
+ return &goalRepository{
+ BaseRepository: BaseRepository{db},
+ }
+}
+
+const (
+ listGoalLevelQuery = "SELECT * from goal;"
+
+ getGoalIdByGoalLevelQuery = "SELECT id from goal where level=$1"
+
+ listUserGoalLevelTargetsQuery = `
+ SELECT * from goal_contribution
+ where goal_id
+ IN
+ (SELECT current_active_goal_id from users where id=$1)`
+
+ createCustomGoalLevelTargetQuery = `
+ INSERT INTO goal_contribution(
+ goal_id,
+ contribution_score_id,
+ target_count,
+ is_custom,
+ set_by_user_id
+ )
+ VALUES
+ ($1, $2, $3, $4, $5)
+ RETURNING *`
+
+ getUserActiveGoalLevelQuery = `
+ SELECT level from goal
+ where id IN
+ (SELECT current_active_goal_id from users where id=$1)`
+)
+
+func (gr *goalRepository) ListGoalLevels(ctx context.Context, tx *sqlx.Tx) ([]Goal, error) {
+ executer := gr.BaseRepository.initiateQueryExecuter(tx)
+
+ var goals []Goal
+ err := executer.SelectContext(ctx, &goals, listGoalLevelQuery)
+ if err != nil {
+ slog.Error("error fetching goal levels", "error", err)
+ return nil, apperrors.ErrFetchingGoals
+ }
+
+ return goals, nil
+}
+
+func (gr *goalRepository) GetGoalIdByGoalLevel(ctx context.Context, tx *sqlx.Tx, level string) (int, error) {
+ executer := gr.BaseRepository.initiateQueryExecuter(tx)
+
+ var goalId int
+ err := executer.GetContext(ctx, &goalId, getGoalIdByGoalLevelQuery, level)
+ if err != nil {
+ if errors.Is(err, sql.ErrNoRows) {
+ slog.Error("error goal not found", "error", err)
+ return 0, apperrors.ErrGoalNotFound
+ }
+
+ slog.Error("error occured while getting goal id by goal level", "error", err)
+ return 0, apperrors.ErrInternalServer
+ }
+
+ return goalId, nil
+}
+
+func (gr *goalRepository) ListUserGoalLevelTargets(ctx context.Context, tx *sqlx.Tx, userId int) ([]GoalContribution, error) {
+ executer := gr.BaseRepository.initiateQueryExecuter(tx)
+
+ var goalLevelTargets []GoalContribution
+ err := executer.SelectContext(ctx, &goalLevelTargets, listUserGoalLevelTargetsQuery, userId)
+ if err != nil {
+ if errors.Is(err, sql.ErrNoRows) {
+ slog.Error("error goal not found", "error", err)
+ return nil, apperrors.ErrInternalServer
+ }
+
+ slog.Error("error occured while getting goal id by goal level", "error", err)
+ return nil, apperrors.ErrInternalServer
+ }
+
+ return goalLevelTargets, nil
+}
+
+func (gr *goalRepository) CreateCustomGoalLevelTarget(ctx context.Context, tx *sqlx.Tx, customGoalContributionInfo GoalContribution) (GoalContribution, error) {
+ executer := gr.BaseRepository.initiateQueryExecuter(tx)
+
+ var customGoalContribution GoalContribution
+ err := executer.GetContext(ctx, &customGoalContribution, createCustomGoalLevelTargetQuery,
+ customGoalContributionInfo.GoalId,
+ customGoalContributionInfo.ContributionScoreId,
+ customGoalContributionInfo.TargetCount,
+ true,
+ customGoalContributionInfo.SetByUserId)
+ if err != nil {
+ slog.Error("error creating custom goal level targets", "error", err)
+ return GoalContribution{}, apperrors.ErrCustomGoalTargetCreationFailed
+ }
+
+ return customGoalContribution, nil
+}
+
+func (gr *goalRepository) GetUserActiveGoalLevel(ctx context.Context, tx *sqlx.Tx, userId int) (string, error) {
+ executer := gr.BaseRepository.initiateQueryExecuter(tx)
+
+ var userActiveGoalLevel string
+ err := executer.GetContext(ctx, &userActiveGoalLevel, getUserActiveGoalLevelQuery, userId)
+ if err != nil {
+ slog.Error("error getting users current active goal level name", "error", err)
+ return userActiveGoalLevel, apperrors.ErrInternalServer
+ }
+
+ return userActiveGoalLevel, nil
+}
diff --git a/backend/internal/repository/repository.go b/backend/internal/repository/repository.go
new file mode 100644
index 00000000..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/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..e75db674
--- /dev/null
+++ b/backend/internal/repository/user.go
@@ -0,0 +1,262 @@
+package repository
+
+import (
+ "context"
+ "database/sql"
+ "errors"
+ "log/slog"
+ "time"
+
+ "github.com/jmoiron/sqlx"
+ "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors"
+)
+
+type userRepository struct {
+ BaseRepository
+}
+
+type UserRepository interface {
+ RepositoryTransaction
+ GetUserById(ctx context.Context, tx *sqlx.Tx, userId int) (User, error)
+ GetUserByGithubId(ctx context.Context, tx *sqlx.Tx, githubId int) (User, error)
+ CreateUser(ctx context.Context, tx *sqlx.Tx, userInfo CreateUserRequestBody) (User, error)
+ UpdateUserEmail(ctx context.Context, tx *sqlx.Tx, userId int, email string) error
+ MarkUserAsDeleted(ctx context.Context, tx *sqlx.Tx, userId int, deletedAt time.Time) error
+ RecoverAccountInGracePeriod(ctx context.Context, tx *sqlx.Tx, userId int) error
+ HardDeleteUsers(ctx context.Context, tx *sqlx.Tx) error
+ GetAllUsersGithubId(ctx context.Context, tx *sqlx.Tx) ([]int, error)
+ UpdateUserCurrentBalance(ctx context.Context, tx *sqlx.Tx, user User) error
+ GetAllUsersRank(ctx context.Context, tx *sqlx.Tx) ([]LeaderboardUser, error)
+ GetCurrentUserRank(ctx context.Context, tx *sqlx.Tx, userId int) (LeaderboardUser, error)
+ UpdateCurrentActiveGoalId(ctx context.Context, tx *sqlx.Tx, userId int, goalId int) (int, error)
+}
+
+func NewUserRepository(db *sqlx.DB) UserRepository {
+ return &userRepository{
+ BaseRepository: BaseRepository{db},
+ }
+}
+
+const (
+ getUserByIdQuery = "SELECT * from users where id=$1"
+
+ getUserByGithubIdQuery = "SELECT * from users where github_id=$1"
+
+ createUserQuery = `
+ INSERT INTO users (
+ github_id,
+ github_username,
+ email,
+ avatar_url
+ )
+ VALUES ($1, $2, $3, $4)
+ RETURNING *`
+
+ updateEmailQuery = "UPDATE users SET email=$1, updated_at=$2 where id=$3"
+
+ markUserAsDeletedQuery = "UPDATE users SET is_deleted = TRUE, deleted_at=$1 where id = $2"
+
+ recoverAccountInGracePeriodQuery = "UPDATE users SET is_deleted = false, deleted_at = NULL where id = $1"
+
+ hardDeleteUsersQuery = "DELETE FROM users WHERE is_deleted = TRUE AND deleted_at <= $1"
+
+ getAllUsersGithubIdQuery = "SELECT github_id from users"
+
+ updateUserCurrentBalanceQuery = "UPDATE users SET current_balance=$1, updated_at=$2 where id=$3"
+
+ getAllUsersRankQuery = `
+ SELECT
+ id,
+ github_username,
+ avatar_url,
+ current_balance,
+ RANK() over (ORDER BY current_balance DESC) AS rank
+ FROM users
+ 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
+ )
+ ranked_users
+ WHERE id = $1;`
+
+ updateCurrentActiveGoalIdQuery = "UPDATE users SET current_active_goal_id=$1 where id=$2"
+)
+
+func (ur *userRepository) GetUserById(ctx context.Context, tx *sqlx.Tx, userId int) (User, error) {
+ executer := ur.BaseRepository.initiateQueryExecuter(tx)
+
+ var user User
+ err := executer.GetContext(ctx, &user, getUserByIdQuery, userId)
+ if err != nil {
+ if errors.Is(err, sql.ErrNoRows) {
+ slog.Error("user not found", "error", err)
+ return User{}, apperrors.ErrUserNotFound
+ }
+ slog.Error("error occurred while getting user by id", "error", err)
+ return User{}, apperrors.ErrInternalServer
+ }
+
+ return user, nil
+}
+
+func (ur *userRepository) GetUserByGithubId(ctx context.Context, tx *sqlx.Tx, githubId int) (User, error) {
+ executer := ur.BaseRepository.initiateQueryExecuter(tx)
+
+ var user User
+ err := executer.GetContext(ctx, &user, getUserByGithubIdQuery, githubId)
+ if err != nil {
+ if errors.Is(err, sql.ErrNoRows) {
+ slog.Error("user not found", "error", err)
+ return User{}, apperrors.ErrUserNotFound
+ }
+ slog.Error("error occurred while getting user by github id", "error", err)
+ return User{}, apperrors.ErrInternalServer
+ }
+
+ return user, nil
+}
+
+func (ur *userRepository) CreateUser(ctx context.Context, tx *sqlx.Tx, userInfo CreateUserRequestBody) (User, error) {
+ executer := ur.BaseRepository.initiateQueryExecuter(tx)
+
+ var user User
+ err := executer.GetContext(ctx, &user, createUserQuery,
+ userInfo.GithubId,
+ userInfo.GithubUsername,
+ userInfo.Email,
+ userInfo.AvatarUrl)
+
+ if err != nil {
+ slog.Error("error occurred while creating user", "error", err)
+ return User{}, apperrors.ErrUserCreationFailed
+ }
+
+ return user, nil
+
+}
+
+func (ur *userRepository) UpdateUserEmail(ctx context.Context, tx *sqlx.Tx, userId int, email string) error {
+ executer := ur.BaseRepository.initiateQueryExecuter(tx)
+
+ _, err := executer.ExecContext(ctx, updateEmailQuery, email, time.Now(), userId)
+ if err != nil {
+ slog.Error("failed to update user email", "error", err)
+ return apperrors.ErrInternalServer
+ }
+
+ return nil
+}
+
+func (ur *userRepository) MarkUserAsDeleted(ctx context.Context, tx *sqlx.Tx, userId int, deletedAt time.Time) error {
+ executer := ur.BaseRepository.initiateQueryExecuter(tx)
+
+ _, err := executer.ExecContext(ctx, markUserAsDeletedQuery, deletedAt, userId)
+ if err != nil {
+ slog.Error("unable to mark user as deleted", "error", err)
+ return apperrors.ErrInternalServer
+ }
+
+ return nil
+}
+
+func (ur *userRepository) RecoverAccountInGracePeriod(ctx context.Context, tx *sqlx.Tx, userId int) error {
+ executer := ur.BaseRepository.initiateQueryExecuter(tx)
+
+ _, err := executer.ExecContext(ctx, recoverAccountInGracePeriodQuery, userId)
+ if err != nil {
+ slog.Error("unable to reverse the soft delete ", "error", err)
+ return apperrors.ErrInternalServer
+ }
+
+ return nil
+}
+
+func (ur *userRepository) HardDeleteUsers(ctx context.Context, tx *sqlx.Tx) error {
+ executer := ur.BaseRepository.initiateQueryExecuter(tx)
+
+ threshold := time.Now().Add(-90 * 1 * time.Second)
+
+ _, err := executer.ExecContext(ctx, hardDeleteUsersQuery, threshold)
+ if err != nil {
+ slog.Error("error deleting users that are soft deleted for more than three months", "error", err)
+ return apperrors.ErrInternalServer
+ }
+
+ return err
+}
+
+func (ur *userRepository) GetAllUsersGithubId(ctx context.Context, tx *sqlx.Tx) ([]int, error) {
+ executer := ur.BaseRepository.initiateQueryExecuter(tx)
+
+ var githubIds []int
+ err := executer.SelectContext(ctx, &githubIds, getAllUsersGithubIdQuery)
+ if err != nil {
+ slog.Error("failed to get github usernames", "error", err)
+ return nil, apperrors.ErrInternalServer
+ }
+
+ return githubIds, nil
+}
+
+func (ur *userRepository) UpdateUserCurrentBalance(ctx context.Context, tx *sqlx.Tx, user User) error {
+ executer := ur.BaseRepository.initiateQueryExecuter(tx)
+
+ _, err := executer.ExecContext(ctx, updateUserCurrentBalanceQuery, user.CurrentBalance, time.Now(), user.Id)
+ if err != nil {
+ slog.Error("failed to update user balance change", "error", err)
+ return apperrors.ErrInternalServer
+ }
+
+ return nil
+}
+
+func (ur *userRepository) GetAllUsersRank(ctx context.Context, tx *sqlx.Tx) ([]LeaderboardUser, error) {
+ executer := ur.BaseRepository.initiateQueryExecuter(tx)
+
+ var leaderboard []LeaderboardUser
+ err := executer.SelectContext(ctx, &leaderboard, getAllUsersRankQuery)
+ if err != nil {
+ slog.Error("failed to get users rank", "error", err)
+ return nil, apperrors.ErrInternalServer
+ }
+
+ return leaderboard, nil
+}
+
+func (ur *userRepository) GetCurrentUserRank(ctx context.Context, tx *sqlx.Tx, userId int) (LeaderboardUser, error) {
+
+ executer := ur.BaseRepository.initiateQueryExecuter(tx)
+
+ var currentUserRank LeaderboardUser
+ err := executer.GetContext(ctx, ¤tUserRank, getCurrentUserRankQuery, userId)
+ if err != nil {
+ slog.Error("failed to get user rank", "error", err)
+ return LeaderboardUser{}, apperrors.ErrInternalServer
+ }
+
+ return currentUserRank, nil
+}
+
+func (ur *userRepository) UpdateCurrentActiveGoalId(ctx context.Context, tx *sqlx.Tx, userId int, goalId int) (int, error) {
+ executer := ur.BaseRepository.initiateQueryExecuter(tx)
+
+ _, err := executer.ExecContext(ctx, updateCurrentActiveGoalIdQuery, goalId, userId)
+ if err != nil {
+ slog.Error("failed to update current active goal id", "error", err)
+ return 0, apperrors.ErrInternalServer
+ }
+
+ return goalId, nil
+}
diff --git a/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/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..7c5230bb
--- /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..8ead3f33
--- /dev/null
+++ b/frontend/package-lock.json
@@ -0,0 +1,5284 @@
+{
+ "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-progress": "^1.1.7",
+ "@radix-ui/react-separator": "^1.1.7",
+ "@radix-ui/react-slot": "^1.2.3",
+ "@tailwindcss/vite": "^4.1.11",
+ "@tanstack/react-query": "^5.83.0",
+ "@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-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/@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/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-primitive": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+ "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-progress": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.7.tgz",
+ "integrity": "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-separator": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz",
+ "integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-beta.27",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
+ "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/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/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-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-refresh": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
+ "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-router": {
+ "version": "7.7.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/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/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/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..f6ce37b5
--- /dev/null
+++ b/frontend/package.json
@@ -0,0 +1,67 @@
+{
+ "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-dropdown-menu": "^2.1.15",
+ "@radix-ui/react-progress": "^1.1.7",
+ "@radix-ui/react-separator": "^1.1.7",
+ "@radix-ui/react-slot": "^1.2.3",
+ "@tailwindcss/vite": "^4.1.11",
+ "@tanstack/react-query": "^5.83.0",
+ "@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-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"
+ },
+ "lint-staged": {
+ "**/*": "prettier --write --ignore-unknown",
+ "**/*.{js,jsx,ts,tsx}": [
+ "npm run lint"
+ ]
+ },
+ "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
+}
diff --git a/frontend/public/vite.svg b/frontend/public/vite.svg
new file mode 100644
index 00000000..e7b8dfb1
--- /dev/null
+++ b/frontend/public/vite.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/src/api/axios.ts b/frontend/src/api/axios.ts
new file mode 100644
index 00000000..9ef6ebc8
--- /dev/null
+++ b/frontend/src/api/axios.ts
@@ -0,0 +1,21 @@
+import axios from "axios";
+
+import { BACKEND_URL } from "@/shared/constants/endpoints";
+import { getAccessToken } from "@/shared/utils/local-storage";
+
+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)
+);
+
+
diff --git a/frontend/src/api/queries/Leaderboard.ts b/frontend/src/api/queries/Leaderboard.ts
new file mode 100644
index 00000000..e1a47cf5
--- /dev/null
+++ b/frontend/src/api/queries/Leaderboard.ts
@@ -0,0 +1,38 @@
+import type { LeaderboardUser } from "@/shared/types/types";
+import { api } from "../axios";
+import { BACKEND_URL } from "@/shared/constants/endpoints";
+import type { ApiResponse } from "@/shared/types/api";
+import { CURRENT_USER_RANK_QUERY_KEY, LEADERBOARD_QUERY_KEY } from "@/shared/constants/query-keys";
+import { useQuery } from "@tanstack/react-query";
+
+const fetchLeaderboard = async (): Promise> => {
+ const response = await api.get<{
+ message: string;
+ data: LeaderboardUser[];
+ }>(`${BACKEND_URL}/api/v1/leaderboard`);
+
+ return response.data;
+}
+
+export const useLeaderboard = () => {
+ return useQuery({
+ queryKey: [LEADERBOARD_QUERY_KEY],
+ queryFn: fetchLeaderboard,
+ });
+}
+
+const fetchCurrentUserRank = async (): Promise> => {
+ const response = await api.get<{
+ message: string;
+ data: LeaderboardUser;
+ }>(`${BACKEND_URL}/api/v1/user/leaderboard`);
+
+ return response.data;
+}
+
+export const useCurrentUserRank = () => {
+ return useQuery({
+ queryKey: [CURRENT_USER_RANK_QUERY_KEY],
+ queryFn: fetchCurrentUserRank,
+ });
+}
\ No newline at end of file
diff --git a/frontend/src/api/queries/Overview.ts b/frontend/src/api/queries/Overview.ts
new file mode 100644
index 00000000..78e3bba4
--- /dev/null
+++ b/frontend/src/api/queries/Overview.ts
@@ -0,0 +1,32 @@
+import type { ApiResponse } from "@/shared/types/api";
+import type { Overview } from "@/shared/types/types";
+import { api } from "../axios";
+import { BACKEND_URL } from "@/shared/constants/endpoints";
+import { useQuery } from "@tanstack/react-query";
+import { OVERVIEW_QUERY_KEY } from "@/shared/constants/query-keys";
+
+interface OverviewParams {
+ year: number;
+ month: number;
+}
+
+const fetchOverview = async ({ year, month }: OverviewParams): Promise> => {
+ const response = await api.get<{
+ message: string;
+ data: Overview[];
+ }>(`${BACKEND_URL}/api/v1/user/overview`, {
+ params: {
+ year,
+ month
+ }
+ });
+
+ return response.data;
+}
+
+export const useOverview = ({ year, month }: OverviewParams) => {
+ return useQuery({
+ queryKey: [OVERVIEW_QUERY_KEY, year, month],
+ queryFn: () => fetchOverview({ year, month }),
+ });
+}
\ No newline at end of file
diff --git a/frontend/src/api/queries/RecentActivities.ts b/frontend/src/api/queries/RecentActivities.ts
new file mode 100644
index 00000000..b39b50db
--- /dev/null
+++ b/frontend/src/api/queries/RecentActivities.ts
@@ -0,0 +1,22 @@
+import type { ApiResponse } from "@/shared/types/api";
+import type { RecentActivity } from "@/shared/types/types";
+import { api } from "../axios";
+import { BACKEND_URL } from "@/shared/constants/endpoints";
+import { useQuery } from "@tanstack/react-query";
+import { RECENT_ACTIVITIES_QUERY_KEY } from "@/shared/constants/query-keys";
+
+const fetchRecentActivities = async (): Promise> => {
+ const response = await api.get<{
+ message: string;
+ data: RecentActivity[];
+ }>(`${BACKEND_URL}/api/v1/user/contributions/all`);
+
+ return response.data;
+};
+
+export const useRecentActivities = () => {
+ return useQuery({
+ queryKey: [RECENT_ACTIVITIES_QUERY_KEY],
+ queryFn: fetchRecentActivities,
+ });
+}
\ No newline at end of file
diff --git a/frontend/src/api/queries/UserBadges.ts b/frontend/src/api/queries/UserBadges.ts
new file mode 100644
index 00000000..9f50e857
--- /dev/null
+++ b/frontend/src/api/queries/UserBadges.ts
@@ -0,0 +1,22 @@
+import { BACKEND_URL } from "@/shared/constants/endpoints";
+import { api } from "../axios";
+import type { ApiResponse } from "@/shared/types/api";
+import type { Badge } from "@/shared/types/types";
+import { USER_BADGES_QUERY_KEY } from "@/shared/constants/query-keys";
+import { useQuery } from "@tanstack/react-query";
+
+const fetchUserBadges = async (): Promise> => {
+ const response = await api.get<{
+ message: string;
+ data: Badge[];
+ }>(`${BACKEND_URL}/api/v1/user/badges`);
+
+ return response.data;
+}
+
+export const useUserBadges = () => {
+ return useQuery({
+ queryKey: [USER_BADGES_QUERY_KEY],
+ queryFn: fetchUserBadges,
+ });
+}
\ No newline at end of file
diff --git a/frontend/src/api/queries/UserProfileDetails.ts b/frontend/src/api/queries/UserProfileDetails.ts
new file mode 100644
index 00000000..3d4639ed
--- /dev/null
+++ b/frontend/src/api/queries/UserProfileDetails.ts
@@ -0,0 +1,22 @@
+import type { User } from "@/shared/types/types";
+import { api } from "../axios";
+import { useQuery } from "@tanstack/react-query";
+import type { ApiResponse } from "@/shared/types/api";
+import { LOGGED_IN_USER_QUERY_KEY } from "@/shared/constants/query-keys";
+import { BACKEND_URL } from "@/shared/constants/endpoints";
+
+const fetchLoggedInUser = async (): Promise> => {
+ const response = await api.get<{
+ message: string;
+ data: User;
+ }>(`${BACKEND_URL}/api/v1/auth/user`);
+
+ return response.data;
+};
+
+export const useLoggedInUser = () => {
+ return useQuery({
+ queryKey: [LOGGED_IN_USER_QUERY_KEY],
+ queryFn: fetchLoggedInUser,
+ });
+};
diff --git a/frontend/src/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/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/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/features/Login/components/LoginComponent.tsx b/frontend/src/features/Login/components/LoginComponent.tsx
new file mode 100644
index 00000000..9084984c
--- /dev/null
+++ b/frontend/src/features/Login/components/LoginComponent.tsx
@@ -0,0 +1,51 @@
+import { Button } from "@/shared/components/ui/button";
+import {
+ Card,
+ CardContent,
+ CardFooter,
+ CardHeader
+} from "@/shared/components/ui/card";
+import { GITHUB_AUTH_URL } from "@/shared/constants/endpoints";
+import Coder from "@/assets/coder.svg";
+import { ACCESS_TOKEN_KEY } from "@/shared/constants/local-storage";
+
+const LoginComponent = () => {
+ const handleGithubLogin = () => {
+ window.location.href = GITHUB_AUTH_URL || "";
+ localStorage.setItem(ACCESS_TOKEN_KEY, 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjIsIklzQWRtaW4iOmZhbHNlLCJleHAiOjE3NTQxMzI3NTh9.VKEboNEvSeVKYnqLuBrvTyvx9IglhYzEyeE57x7Qzto')
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ No idea where to start? Try this{" "}
+
+ Code Triage
+
+
+
+
+ );
+};
+
+export default LoginComponent;
diff --git a/frontend/src/features/Login/index.tsx b/frontend/src/features/Login/index.tsx
new file mode 100644
index 00000000..4114e41d
--- /dev/null
+++ b/frontend/src/features/Login/index.tsx
@@ -0,0 +1,12 @@
+import AuthLayout from "@/shared/layout/AuthLayout";
+import LoginComponent from "@/features/Login/components/LoginComponent";
+
+const Login = () => {
+ return (
+
+
+
+ );
+};
+
+export default Login;
diff --git a/frontend/src/features/MyContributions/index.tsx b/frontend/src/features/MyContributions/index.tsx
new file mode 100644
index 00000000..3a454a7c
--- /dev/null
+++ b/frontend/src/features/MyContributions/index.tsx
@@ -0,0 +1,7 @@
+import UserDashboardLayout from "@/shared/layout/UserDashboardLayout";
+
+const MyContributions = () => {
+ return ;
+};
+
+export default MyContributions;
diff --git a/frontend/src/features/UserDashboard/components/Leaderboard.tsx b/frontend/src/features/UserDashboard/components/Leaderboard.tsx
new file mode 100644
index 00000000..b1482e45
--- /dev/null
+++ b/frontend/src/features/UserDashboard/components/Leaderboard.tsx
@@ -0,0 +1,87 @@
+import { useState, type FC } from "react";
+import clsx from "clsx";
+
+import { Button } from "@/shared/components/ui/button";
+import { Card } from "@/shared/components/ui/card";
+import LeaderboardCard from "@/features/UserDashboard/components/LeaderboardCard";
+import { useCurrentUserRank, useLeaderboard } from "@/api/queries/Leaderboard";
+import { TrendingUp } from "lucide-react";
+
+interface LeaderboardProps {
+ className?: string;
+}
+
+const Leaderboard: FC = ({ className }) => {
+ const [viewAll, setViewAll] = useState(false);
+
+ const handleViewAll = () => {
+ setViewAll(!viewAll);
+ };
+
+ const { data, isLoading } = useLeaderboard();
+ const leaderboard = data?.data ?? [];
+
+ const leaderboardData = viewAll ? leaderboard : leaderboard?.slice(0, 10);
+
+ const { data: userData } = useCurrentUserRank();
+ const currentUser = userData?.data;
+
+ return (
+
+
+
Leader Board
+
+
+
+ {isLoading ? (
+
+ ) : leaderboardData?.length === 0 ? (
+
+
+
+ No leaderboard data
+
+
+ ) : (
+ <>
+
+ {leaderboardData?.map(user => (
+
+ ))}
+
+ {!viewAll && (
+
+
+
+ )}
+ >
+ )}
+
+ );
+};
+
+export default Leaderboard;
diff --git a/frontend/src/features/UserDashboard/components/LeaderboardCard.tsx b/frontend/src/features/UserDashboard/components/LeaderboardCard.tsx
new file mode 100644
index 00000000..f2171616
--- /dev/null
+++ b/frontend/src/features/UserDashboard/components/LeaderboardCard.tsx
@@ -0,0 +1,46 @@
+import type { FC } from "react";
+
+import Coin from "@/shared/components/common/Coin";
+import { Card, CardContent } from "@/shared/components/ui/card";
+
+interface LeaderboardCardProps {
+ rank: number;
+ username: string;
+ repositories: number;
+ balance: number;
+}
+
+const LeaderboardCard: FC = ({
+ rank,
+ username,
+ repositories,
+ balance
+}) => {
+ return (
+
+
+
+
+ {rank}
+
+
+
{username}
+
+ Contributed to{" "}
+
+ {repositories}{" "}
+ {repositories > 1 ? "Repositories" : "Repository"}
+
+
+
+
+
+ {balance}
+
+
+
+
+ );
+};
+
+export default LeaderboardCard;
\ No newline at end of file
diff --git a/frontend/src/features/UserDashboard/components/Overview.tsx b/frontend/src/features/UserDashboard/components/Overview.tsx
new file mode 100644
index 00000000..daeb90b1
--- /dev/null
+++ b/frontend/src/features/UserDashboard/components/Overview.tsx
@@ -0,0 +1,118 @@
+import { useState, type FC } from "react";
+import clsx from "clsx";
+import { Card } from "@/shared/components/ui/card";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger
+} from "@/shared/components/ui/dropdown-menu";
+import OverviewCard from "@/features/UserDashboard/components/OverviewCard";
+import { ChevronDown, TrendingUp } from "lucide-react";
+import { format, subMonths } from "date-fns";
+import { useOverview } from "@/api/queries/Overview";
+
+interface OverviewProps {
+ className?: string;
+}
+
+const getLastNMonths = (n: number) => {
+ return Array.from({ length: n }).map((_, i) => {
+ const date = subMonths(new Date(), i);
+ return {
+ label: format(date, "MMMM yyyy"),
+ month: date.getMonth() + 1,
+ year: date.getFullYear()
+ };
+ });
+};
+
+const Overview: FC = ({ className }) => {
+ const monthOptions = getLastNMonths(3);
+ const [selectedPeriod, setSelectedPeriod] = useState<{
+ month: number;
+ year: number;
+ }>(monthOptions[0]);
+
+ const { data, isLoading } = useOverview(selectedPeriod);
+ const overview = data?.data ?? [];
+
+ const overviewData = overview?.filter(data => {
+ const date = new Date(data.month);
+ return (
+ date.getFullYear() === selectedPeriod.year &&
+ date.getMonth() + 1 === selectedPeriod.month
+ );
+ });
+
+ return (
+
+
+
Overview
+
+
+ {monthOptions.find(
+ opt =>
+ opt.month === selectedPeriod.month &&
+ opt.year === selectedPeriod.year
+ )?.label ?? "Select Month"}
+
+
+
+ {monthOptions.map(option => (
+
+ setSelectedPeriod({ month: option.month, year: option.year })
+ }
+ className={clsx(
+ "cursor-pointer rounded-sm px-3 py-2 text-sm hover:bg-gray-100",
+ selectedPeriod.month === option.month &&
+ selectedPeriod.year === option.year &&
+ "bg-cc-app-gray-background"
+ )}
+ >
+ {option.label}
+
+ ))}
+
+
+
+ {isLoading ? (
+
+ ) : overviewData?.length === 0 ? (
+
+
+
+ No overview data
+
+
+ No activity found for the selected period.
+
+ Try selecting a different month or start contributing!
+
+
+ ) : (
+
+ {overviewData?.map(user => (
+
+ ))}
+
+ )}
+
+ );
+};
+
+export default Overview;
diff --git a/frontend/src/features/UserDashboard/components/OverviewCard.tsx b/frontend/src/features/UserDashboard/components/OverviewCard.tsx
new file mode 100644
index 00000000..81e19767
--- /dev/null
+++ b/frontend/src/features/UserDashboard/components/OverviewCard.tsx
@@ -0,0 +1,32 @@
+import { type FC } from "react";
+import Coin from "@/shared/components/common/Coin";
+import { Card } from "@/shared/components/ui/card";
+
+interface OverviewCardProps {
+ type: string;
+ count: number;
+ totalCoins: number;
+}
+
+const OverviewCard: FC = ({ type, count, totalCoins }) => {
+ return (
+
+
+
{type}
+
+
+ {count}
+
+
+
+
+ {totalCoins}
+
+
+
+
+
+ );
+};
+
+export default OverviewCard;
diff --git a/frontend/src/features/UserDashboard/components/RecentActivities.tsx b/frontend/src/features/UserDashboard/components/RecentActivities.tsx
new file mode 100644
index 00000000..614807a0
--- /dev/null
+++ b/frontend/src/features/UserDashboard/components/RecentActivities.tsx
@@ -0,0 +1,89 @@
+import { useState, type FC } from "react";
+import clsx from "clsx";
+import { Button } from "@/shared/components/ui/button";
+import { Card } from "@/shared/components/ui/card";
+import ActivityCard from "@/shared/components/common/ActivityCard";
+import { useRecentActivities } from "@/api/queries/RecentActivities";
+import { Link } from "react-router-dom";
+import { TrendingUp } from "lucide-react";
+
+interface RecentActivitiesProps {
+ className?: string;
+}
+
+const RecentActivities: FC = ({ className }) => {
+ const [viewAll, setViewAll] = useState(false);
+
+ const handleViewAll = () => {
+ setViewAll(!viewAll);
+ };
+
+ const { data, isLoading } = useRecentActivities();
+ const recentActivities = data?.data ?? [];
+ const recentActivitiesData = viewAll
+ ? recentActivities
+ : recentActivities?.slice(0, 4);
+
+ return (
+
+
+
Recent Activities
+
+
+
+ {isLoading ? (
+
+ ) : recentActivitiesData?.length === 0 ? (
+
+
+
+ No recent activities found
+
+
+ ) : (
+
+ {recentActivitiesData?.map((activity, index) => (
+
+ ))}
+ {!viewAll && (
+
+
+ How does points work?
+
+
+ )}
+
+ )}
+
+ );
+};
+
+export default RecentActivities;
diff --git a/frontend/src/features/UserDashboard/components/UserDashboardComponent.tsx b/frontend/src/features/UserDashboard/components/UserDashboardComponent.tsx
new file mode 100644
index 00000000..176d2692
--- /dev/null
+++ b/frontend/src/features/UserDashboard/components/UserDashboardComponent.tsx
@@ -0,0 +1,17 @@
+import Leaderboard from "@/features/UserDashboard/components/Leaderboard";
+import RecentActivities from "@/features/UserDashboard/components/RecentActivities";
+import Overview from "@/features/UserDashboard/components/Overview";
+
+const UserDashboardComponent = () => {
+ return (
+
+ );
+};
+
+export default UserDashboardComponent;
diff --git a/frontend/src/features/UserDashboard/index.tsx b/frontend/src/features/UserDashboard/index.tsx
new file mode 100644
index 00000000..dcc63321
--- /dev/null
+++ b/frontend/src/features/UserDashboard/index.tsx
@@ -0,0 +1,12 @@
+import UserDashboardLayout from "@/shared/layout/UserDashboardLayout";
+import UserDashboardComponent from "@/features/UserDashboard/components/UserDashboardComponent";
+
+const UserDashboard = () => {
+ return (
+
+
+
+ );
+};
+
+export default UserDashboard;
diff --git a/frontend/src/index.css b/frontend/src/index.css
new file mode 100644
index 00000000..d277bc17
--- /dev/null
+++ b/frontend/src/index.css
@@ -0,0 +1,172 @@
+@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);
+}
+
+@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);
+}
+
+.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..817fad21
--- /dev/null
+++ b/frontend/src/main.tsx
@@ -0,0 +1,20 @@
+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 { AuthProvider } from "@/shared/context/AuthProvider";
+import { queryClient } from "@/api/react-query.ts";
+import "./index.css";
+
+createRoot(document.getElementById("root")!).render(
+
+
+
+
+
+
+
+
+);
diff --git a/frontend/src/root/Router.tsx b/frontend/src/root/Router.tsx
new file mode 100644
index 00000000..dc00eced
--- /dev/null
+++ b/frontend/src/root/Router.tsx
@@ -0,0 +1,23 @@
+import { RouterProvider, createBrowserRouter } from "react-router-dom";
+
+import WithAuth from "@/shared/HOC/WithAuth";
+import { type RoutesType, routesConfig } from "@/root/routes-config";
+
+const generateRoutes = (routes: RoutesType[]) => {
+ return routes.map(({ path, element, isProtected }) => {
+ let wrappedElement = element;
+
+ if (isProtected) {
+ wrappedElement = {wrappedElement};
+ }
+
+ 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..62633f1f
--- /dev/null
+++ b/frontend/src/root/routes-config.tsx
@@ -0,0 +1,34 @@
+import type { ReactNode } from "react";
+
+import Login from "@/features/Login";
+import MyContributions from "@/features/MyContributions";
+import UserDashboard from "@/features/UserDashboard";
+import {
+ LOGIN_PATH,
+ MY_CONTRIBUTIONS_PATH,
+ USER_DASHBOARD_PATH
+} from "@/shared/constants/routes";
+
+export interface RoutesType {
+ path: string;
+ element: ReactNode;
+ isProtected?: boolean;
+}
+
+export const routesConfig: RoutesType[] = [
+ {
+ path: LOGIN_PATH,
+ element: ,
+ isProtected: false
+ },
+ {
+ path: USER_DASHBOARD_PATH,
+ element: ,
+ isProtected: false
+ },
+ {
+ path: MY_CONTRIBUTIONS_PATH,
+ element: ,
+ isProtected: false
+ }
+];
diff --git a/frontend/src/shared/HOC/WithAuth.tsx b/frontend/src/shared/HOC/WithAuth.tsx
new file mode 100644
index 00000000..fb44e44a
--- /dev/null
+++ b/frontend/src/shared/HOC/WithAuth.tsx
@@ -0,0 +1,29 @@
+import { type FC, type ReactNode, useEffect } from "react";
+import { Navigate, useLocation, useNavigate } from "react-router-dom";
+
+import { LOGIN_PATH } from "@/shared/constants/routes";
+import { getAccessToken } from "@/shared/utils/local-storage";
+
+interface WithAuthProps {
+ children: ReactNode;
+}
+
+const WithAuth: FC = ({ children }) => {
+ const navigate = useNavigate();
+ const location = useLocation();
+ const userAccessToken = getAccessToken();
+
+ useEffect(() => {
+ if (!userAccessToken) {
+ navigate(LOGIN_PATH, { replace: true });
+ }
+ }, [userAccessToken, location.pathname, navigate]);
+
+ if (!userAccessToken) {
+ return ;
+ }
+
+ return <>{children}>;
+};
+
+export default WithAuth;
diff --git a/frontend/src/shared/components/UserDashboard/Navbar.tsx b/frontend/src/shared/components/UserDashboard/Navbar.tsx
new file mode 100644
index 00000000..b509d225
--- /dev/null
+++ b/frontend/src/shared/components/UserDashboard/Navbar.tsx
@@ -0,0 +1,30 @@
+import { Link, useLocation } from "react-router-dom";
+
+import { Card } from "@/shared/components/ui/card";
+import { USER_DASHBOARD_NAVBAR_OPTIONS } from "@/shared/types/navbar";
+
+const Navbar = () => {
+ const location = useLocation();
+
+ const isActive = (path: string) => location.pathname === path;
+
+ return (
+
+ {USER_DASHBOARD_NAVBAR_OPTIONS.map(option => (
+
+ {option.name}
+
+ ))}
+
+ );
+};
+
+export default Navbar;
diff --git a/frontend/src/shared/components/UserDashboard/UserBadges.tsx b/frontend/src/shared/components/UserDashboard/UserBadges.tsx
new file mode 100644
index 00000000..0cc35a75
--- /dev/null
+++ b/frontend/src/shared/components/UserDashboard/UserBadges.tsx
@@ -0,0 +1,50 @@
+import { useUserBadges } from "@/api/queries/UserBadges";
+import { Star } from "lucide-react";
+import type { Badge } from "@/shared/types/types";
+
+const badgeColorMap: Record = {
+ BEGINNER: "text-[#cd7f32]",
+ INTERMEDIATE: "text-[#c0c0c0]",
+ ADVANCED: "text-[#ffd700]",
+ CUSTOM: "text-orange-400",
+};
+
+const UserBadges = () => {
+ const { data } = useUserBadges();
+ const badges = data?.data ?? [];
+
+ const grouped = badges.reduce>((acc, badge) => {
+ const type = badge.badgeType.toUpperCase();
+ if (!acc[type]) acc[type] = [];
+ acc[type].push(badge);
+ return acc;
+ }, {});
+
+ return (
+
+
+ BADGES
+
+
+ {Object.entries(grouped).map(([type, badgeList]) => {
+ const color = badgeColorMap[type] ?? "text-gray-400";
+ return (
+
+
+ {badgeList.length > 1 && (
+
×{badgeList.length}
+ )}
+
+ {type}
+
+
+ );
+ })}
+
+
+ );
+};
+
+export default UserBadges;
diff --git a/frontend/src/shared/components/UserDashboard/UserGoals.tsx b/frontend/src/shared/components/UserDashboard/UserGoals.tsx
new file mode 100644
index 00000000..f6cdd95c
--- /dev/null
+++ b/frontend/src/shared/components/UserDashboard/UserGoals.tsx
@@ -0,0 +1,42 @@
+import { Progress } from "@/shared/components/ui/progress";
+const goals = [
+ { name: "Issue Resolve", current: 2, total: 5, progress: 40 },
+ { name: "PR Review", current: 6, total: 8, progress: 75 },
+ { name: "PR Merge", current: 2, total: 2, progress: 100 },
+ { name: "PR Close", current: 1, total: 5, progress: 20 },
+ { name: "PR Close", current: 1, total: 5, progress: 20 },
+ { name: "PR Close", current: 1, total: 5, progress: 20 },
+ { name: "PR Close", current: 1, total: 5, progress: 20 },
+ { name: "PR Close", current: 1, total: 5, progress: 20 },
+ { name: "PR Close", current: 1, total: 5, progress: 20 },
+ { name: "PR Close", current: 1, total: 5, progress: 20 }
+];
+
+const UserGoals = () => {
+ return (
+
+
+ MY GOALS (BEGINNER)
+
+
+ {goals.map((goal, index) => (
+
+
+ {goal.name}
+
+ {goal.current}/{goal.total}
+
+
+
+
+ ))}
+
+
+ );
+};
+
+export default UserGoals;
diff --git a/frontend/src/shared/components/UserDashboard/UserProfileCard.tsx b/frontend/src/shared/components/UserDashboard/UserProfileCard.tsx
new file mode 100644
index 00000000..36918e54
--- /dev/null
+++ b/frontend/src/shared/components/UserDashboard/UserProfileCard.tsx
@@ -0,0 +1,28 @@
+import { Card } from "@/shared/components/ui/card";
+import { Separator } from "@/shared/components/ui/separator";
+import UserProfileDetails from "@/shared/components/UserDashboard/UserProfileDetails";
+import UserBadges from "@/shared/components/UserDashboard/UserBadges";
+import UserGoals from "@/shared/components/UserDashboard/UserGoals";
+
+const UserProfileCard = () => {
+ return (
+
+
+
+
+ {Array.from({ length: 30 }).map((_, i) => (
+
+ ))}
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default UserProfileCard;
diff --git a/frontend/src/shared/components/UserDashboard/UserProfileDetails.tsx b/frontend/src/shared/components/UserDashboard/UserProfileDetails.tsx
new file mode 100644
index 00000000..111faf49
--- /dev/null
+++ b/frontend/src/shared/components/UserDashboard/UserProfileDetails.tsx
@@ -0,0 +1,62 @@
+import { ExternalLink, MoreHorizontal } from "lucide-react";
+
+import Coin from "@/shared/components/common/Coin";
+import { Button } from "@/shared/components/ui/button";
+import DefaultProfilePic from "@/assets/default-profile-pic.svg"
+import { Separator } from "@/shared/components/ui/separator";
+import { useLoggedInUser } from "@/api/queries/UserProfileDetails";
+import { Link } from "react-router-dom";
+
+const UserProfileDetails = () => {
+ const { data } = useLoggedInUser();
+ const user = data?.data
+
+ return (
+
+
+
+
+

+
+
+
+
+
{user?.githubUsername || "username"}
+
+
+
+
+
+
+
+
+
+
+ {user?.currentBalance || "0"}
+
+
+
+
+
+
+
+ );
+};
+
+export default UserProfileDetails;
diff --git a/frontend/src/shared/components/common/ActivityCard.tsx b/frontend/src/shared/components/common/ActivityCard.tsx
new file mode 100644
index 00000000..5d55a755
--- /dev/null
+++ b/frontend/src/shared/components/common/ActivityCard.tsx
@@ -0,0 +1,58 @@
+import type { FC } from "react";
+import Coin from "@/shared/components/common/Coin";
+
+interface ActivityCardProps {
+ contributionType: string;
+ repositoryName: string;
+ contributedAt: string;
+ balanceChange: number;
+ showLine: boolean;
+}
+
+const ActivityCard: FC = ({
+ contributionType,
+ repositoryName,
+ contributedAt,
+ balanceChange,
+ showLine = true
+}) => {
+ return (
+
+ {showLine && (
+
+ )}
+
+
+
+
+
+
+ {contributionType}
+
+
+ Contributed to{" "}
+
+ <{repositoryName}>
+
+
+
+ Contributed on {contributedAt}
+
+
+
+ {balanceChange && (
+
+
+ {balanceChange < 0 ? `-${Math.abs(balanceChange)}` : balanceChange}
+
+ )}
+
+
+ );
+};
+
+export default ActivityCard;
diff --git a/frontend/src/shared/components/common/Coin.tsx b/frontend/src/shared/components/common/Coin.tsx
new file mode 100644
index 00000000..e85fdbdf
--- /dev/null
+++ b/frontend/src/shared/components/common/Coin.tsx
@@ -0,0 +1,9 @@
+const Coin = () => {
+ return (
+
+ );
+};
+
+export default Coin;
diff --git a/frontend/src/shared/components/ui/button.tsx b/frontend/src/shared/components/ui/button.tsx
new file mode 100644
index 00000000..61ab42c9
--- /dev/null
+++ b/frontend/src/shared/components/ui/button.tsx
@@ -0,0 +1,63 @@
+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",
+ destructive:
+ "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
+ outline:
+ "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
+ secondary:
+ "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
+ ghost:
+ "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
+ link: "text-primary underline-offset-4 hover:underline",
+ ccAppOutlineMidBlue:
+ "bg-cc-app-mid-blue hover:bg-cc-app-blue rounded-sm border border-white text-white",
+ ccAppOutline:
+ "border-cc-app-mid-blue text-cc-app-blue hover:bg-cc-app-mid-blue/5 rounded-sm border focus:outline-none"
+ },
+ size: {
+ 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/dropdown-menu.tsx b/frontend/src/shared/components/ui/dropdown-menu.tsx
new file mode 100644
index 00000000..05a5157c
--- /dev/null
+++ b/frontend/src/shared/components/ui/dropdown-menu.tsx
@@ -0,0 +1,255 @@
+import * as React from "react"
+import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
+import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
+
+import { cn } from "@/shared/utils/tailwindcss"
+
+function DropdownMenu({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function DropdownMenuPortal({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function DropdownMenuTrigger({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function DropdownMenuContent({
+ className,
+ sideOffset = 4,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+ )
+}
+
+function DropdownMenuGroup({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function DropdownMenuItem({
+ className,
+ inset,
+ variant = "default",
+ ...props
+}: React.ComponentProps & {
+ inset?: boolean
+ variant?: "default" | "destructive"
+}) {
+ return (
+
+ )
+}
+
+function DropdownMenuCheckboxItem({
+ className,
+ children,
+ checked,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+
+
+
+ {children}
+
+ )
+}
+
+function DropdownMenuRadioGroup({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function DropdownMenuRadioItem({
+ className,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+
+
+
+ {children}
+
+ )
+}
+
+function DropdownMenuLabel({
+ className,
+ inset,
+ ...props
+}: React.ComponentProps & {
+ inset?: boolean
+}) {
+ return (
+
+ )
+}
+
+function DropdownMenuSeparator({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function DropdownMenuShortcut({
+ className,
+ ...props
+}: React.ComponentProps<"span">) {
+ return (
+
+ )
+}
+
+function DropdownMenuSub({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function DropdownMenuSubTrigger({
+ className,
+ inset,
+ children,
+ ...props
+}: React.ComponentProps & {
+ inset?: boolean
+}) {
+ return (
+
+ {children}
+
+
+ )
+}
+
+function DropdownMenuSubContent({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export {
+ DropdownMenu,
+ DropdownMenuPortal,
+ DropdownMenuTrigger,
+ DropdownMenuContent,
+ DropdownMenuGroup,
+ DropdownMenuLabel,
+ DropdownMenuItem,
+ DropdownMenuCheckboxItem,
+ DropdownMenuRadioGroup,
+ DropdownMenuRadioItem,
+ DropdownMenuSeparator,
+ DropdownMenuShortcut,
+ DropdownMenuSub,
+ DropdownMenuSubTrigger,
+ DropdownMenuSubContent,
+}
diff --git a/frontend/src/shared/components/ui/progress.tsx b/frontend/src/shared/components/ui/progress.tsx
new file mode 100644
index 00000000..d2110be8
--- /dev/null
+++ b/frontend/src/shared/components/ui/progress.tsx
@@ -0,0 +1,35 @@
+import * as React from "react";
+import * as ProgressPrimitive from "@radix-ui/react-progress";
+
+import { cn } from "@/shared/utils/tailwindcss";
+
+function Progress({
+ className,
+ indicatorClassName,
+ value,
+ ...props
+}: React.ComponentProps & {
+ indicatorClassName?: string;
+}) {
+ return (
+
+
+
+ );
+}
+
+export { Progress };
diff --git a/frontend/src/shared/components/ui/separator.tsx b/frontend/src/shared/components/ui/separator.tsx
new file mode 100644
index 00000000..70af3ab9
--- /dev/null
+++ b/frontend/src/shared/components/ui/separator.tsx
@@ -0,0 +1,26 @@
+import * as React from "react";
+import * as SeparatorPrimitive from "@radix-ui/react-separator";
+
+import { cn } from "@/shared/utils/tailwindcss";
+
+function Separator({
+ className,
+ orientation = "horizontal",
+ decorative = true,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+export { Separator };
diff --git a/frontend/src/shared/components/ui/sonner.tsx b/frontend/src/shared/components/ui/sonner.tsx
new file mode 100644
index 00000000..85514eca
--- /dev/null
+++ b/frontend/src/shared/components/ui/sonner.tsx
@@ -0,0 +1,23 @@
+import { useTheme } from "next-themes";
+import { Toaster as Sonner, type ToasterProps } from "sonner";
+
+const Toaster = ({ ...props }: ToasterProps) => {
+ const { theme = "system" } = useTheme();
+
+ return (
+
+ );
+};
+
+export { Toaster };
diff --git a/frontend/src/shared/constants/endpoints.ts b/frontend/src/shared/constants/endpoints.ts
new file mode 100644
index 00000000..ac10321c
--- /dev/null
+++ b/frontend/src/shared/constants/endpoints.ts
@@ -0,0 +1,3 @@
+export const GITHUB_AUTH_URL = import.meta.env.VITE_GITHUB_AUTH_URL as string;
+export const BACKEND_URL = import.meta.env.VITE_BACKEND_URL as string;
+export const FRONTEND_URL = import.meta.env.VITE_FRONTEND_URL as string;
diff --git a/frontend/src/shared/constants/local-storage.ts b/frontend/src/shared/constants/local-storage.ts
new file mode 100644
index 00000000..964b3b29
--- /dev/null
+++ b/frontend/src/shared/constants/local-storage.ts
@@ -0,0 +1 @@
+export const ACCESS_TOKEN_KEY = "cc-7db23e66-accessToken";
diff --git a/frontend/src/shared/constants/query-keys.ts b/frontend/src/shared/constants/query-keys.ts
new file mode 100644
index 00000000..5f5b57c5
--- /dev/null
+++ b/frontend/src/shared/constants/query-keys.ts
@@ -0,0 +1,6 @@
+export const LOGGED_IN_USER_QUERY_KEY = "logged-in-user"
+export const USER_BADGES_QUERY_KEY = "user-badges"
+export const LEADERBOARD_QUERY_KEY="leaderboard"
+export const CURRENT_USER_RANK_QUERY_KEY="current-user-rank"
+export const RECENT_ACTIVITIES_QUERY_KEY="recent-activities"
+export const OVERVIEW_QUERY_KEY="overview"
\ No newline at end of file
diff --git a/frontend/src/shared/constants/routes.ts b/frontend/src/shared/constants/routes.ts
new file mode 100644
index 00000000..8cbdabfc
--- /dev/null
+++ b/frontend/src/shared/constants/routes.ts
@@ -0,0 +1,4 @@
+export const LOGIN_PATH = "/login";
+
+export const USER_DASHBOARD_PATH = "/";
+export const MY_CONTRIBUTIONS_PATH = "/my-contributions";
diff --git a/frontend/src/shared/context/AuthProvider.tsx b/frontend/src/shared/context/AuthProvider.tsx
new file mode 100644
index 00000000..a5e1340f
--- /dev/null
+++ b/frontend/src/shared/context/AuthProvider.tsx
@@ -0,0 +1,53 @@
+import { type ReactNode, createContext, useMemo, useState } from "react";
+
+import { clearAccessToken, setAccessToken } from "@/shared/utils/local-storage";
+
+export type UserCredentials = {
+ githubId: string;
+ githubUsername: string;
+ avatarUrl: string;
+};
+
+export interface AuthContextInterface {
+ userCredentials: UserCredentials | null;
+ login: (userCredentials: UserCredentials, token: string) => void;
+ logout: () => void;
+}
+
+const AuthContext = createContext({
+ userCredentials: null,
+ login: () => {
+ throw new Error("AuthContext: login called outside AuthProvider");
+ },
+ logout: () => {
+ throw new Error("AuthContext: logout called outside AuthProvider");
+ }
+});
+
+type AuthProviderProps = {
+ children: ReactNode;
+};
+
+export const AuthProvider = ({ children }: AuthProviderProps) => {
+ const [userCredentials, setUserCredentials] =
+ useState(null);
+
+ const login = (userCredentials: UserCredentials, token: string) => {
+ setUserCredentials(userCredentials);
+ setAccessToken(token);
+ };
+
+ const logout = () => {
+ setUserCredentials(null);
+ clearAccessToken();
+ };
+
+ const value = useMemo(
+ () => ({ userCredentials, login, logout }),
+ [userCredentials]
+ );
+
+ return {children};
+};
+
+export { AuthContext };
diff --git a/frontend/src/shared/layout/AuthLayout.tsx b/frontend/src/shared/layout/AuthLayout.tsx
new file mode 100644
index 00000000..9cbad0b2
--- /dev/null
+++ b/frontend/src/shared/layout/AuthLayout.tsx
@@ -0,0 +1,86 @@
+import { type FC, type ReactNode, useEffect } from "react";
+import { useLocation, useNavigate } from "react-router-dom";
+import { CheckCircle } from "lucide-react";
+
+import { Card } from "@/shared/components/ui/card";
+import { LOGIN_PATH, USER_DASHBOARD_PATH } from "@/shared/constants/routes";
+import { getAccessToken } from "@/shared/utils/local-storage";
+
+interface AuthLayoutProps {
+ children: ReactNode;
+}
+
+const AuthLayout: FC = ({ children }) => {
+ const location = useLocation();
+ const navigate = useNavigate();
+
+ useEffect(() => {
+ const userAccessToken = getAccessToken();
+ const shouldRedirect = [LOGIN_PATH].includes(location.pathname);
+
+ if (userAccessToken && shouldRedirect) {
+ navigate(USER_DASHBOARD_PATH);
+ }
+ }, [navigate, location]);
+
+ return (
+
+
+
+
+
+ {Array.from({ length: 64 }).map((_, i) => (
+
+ ))}
+
+
+
+
+
+
+
+
+ Code Curiosity
+
+
+
+
+ {[
+ "Earn and Upskill",
+ "Set Your Goals",
+ "Leader Board",
+ "Open Source Contribution"
+ ].map((text, i) => (
+
+
+
+ {text}
+
+
+ ))}
+
+
+
+
+
+
+
+ {Array.from({ length: 64 }).map((_, i) => (
+
+ ))}
+
+
+
+
+
+ {children}
+
+
+
+ );
+};
+
+export default AuthLayout;
diff --git a/frontend/src/shared/layout/UserDashboardLayout.tsx b/frontend/src/shared/layout/UserDashboardLayout.tsx
new file mode 100644
index 00000000..fd087c22
--- /dev/null
+++ b/frontend/src/shared/layout/UserDashboardLayout.tsx
@@ -0,0 +1,24 @@
+import type { FC, ReactNode } from "react";
+
+import Navbar from "@/shared/components/UserDashboard/Navbar";
+import UserProfileCard from "@/shared/components/UserDashboard/UserProfileCard";
+
+interface UserDashboardLayoutProps {
+ children?: ReactNode;
+}
+
+const UserDashboardLayout: FC = ({ children }) => {
+ return (
+
+ );
+};
+
+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..157b6784
--- /dev/null
+++ b/frontend/src/shared/types/navbar.ts
@@ -0,0 +1,6 @@
+import { MY_CONTRIBUTIONS_PATH, USER_DASHBOARD_PATH } from "@/shared/constants/routes";
+
+export const USER_DASHBOARD_NAVBAR_OPTIONS = [
+ { name: "Dashboard", path: USER_DASHBOARD_PATH },
+ { name: "My Contributions", path: MY_CONTRIBUTIONS_PATH }
+];
diff --git a/frontend/src/shared/types/types.ts b/frontend/src/shared/types/types.ts
new file mode 100644
index 00000000..d53b24f2
--- /dev/null
+++ b/frontend/src/shared/types/types.ts
@@ -0,0 +1,58 @@
+export interface User {
+ userId: number;
+ githubId: number;
+ githubUsername: string;
+ email: string;
+ avatarUrl: string;
+ currentBalance: number;
+ currentActiveGoalId: number;
+ isBlocked: boolean;
+ isAdmin: boolean;
+ password: string;
+ isDeleted: boolean;
+ deletedAt: string;
+ createdAt: string;
+ updatedAt: string;
+}
+
+export interface Badge {
+ id: number;
+ userId: number;
+ badgeType: string;
+ earnedAt: string;
+ createdAt: string;
+}
+
+export interface LeaderboardUser {
+ id: number;
+ githubUsername: string;
+ avatarUrl: string;
+ contributedReposCount: number;
+ currentBalance: number;
+ rank: number;
+}
+
+export interface RecentActivity {
+ userId: number;
+ repositoryId: number;
+ contributionScoreId: number;
+ contributionType: string;
+ balanceChange: number;
+ contributedAt: string;
+ githubEventId: number;
+ githubRepoId: number;
+ repoName: string;
+ description: string;
+ languagesUrl: string;
+ repoUrl: string;
+ ownerName: string;
+ updateDate: string;
+ contributorsUrl: string;
+}
+
+export interface Overview {
+ type: string;
+ count: number;
+ totalCoins: number;
+ month: string;
+}
diff --git a/frontend/src/shared/utils/local-storage.ts b/frontend/src/shared/utils/local-storage.ts
new file mode 100644
index 00000000..455701cd
--- /dev/null
+++ b/frontend/src/shared/utils/local-storage.ts
@@ -0,0 +1,13 @@
+import { ACCESS_TOKEN_KEY } from "@/shared/constants/local-storage";
+
+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);
+};
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..0e4ba5f0
--- /dev/null
+++ b/frontend/tailwind.config.js
@@ -0,0 +1,63 @@
+/** @type {import('tailwindcss').Config} */
+module.exports = {
+ content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
+ theme: {
+ extend: {
+ borderRadius: {
+ lg: "var(--radius)",
+ md: "calc(var(--radius) - 2px)",
+ sm: "calc(var(--radius) - 4px)"
+ },
+ colors: {
+ cclightblue: "hsl(var(--cc-app-light-blue))",
+ ccskyblue: "hsl(var(--cc-app-sky-blue))",
+ ccmidblue: "hsl(var(--cc-app-mid-blue))",
+ ccappblue: "hsl(var(--cc-app-blue))",
+ ccappgraybackground: "hsl(var(--cc-app-gray-background))",
+ ccapporange: "hsl(var(--cc-app-orange))",
+
+ background: "hsl(var(--background))",
+ foreground: "hsl(var(--foreground))",
+ 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": {}
+}