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 ( + + + Developer Illustration + + + + +
+
+ + +

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

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

Leader Board

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

+ No leaderboard data +

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

Overview

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

+ No overview data +

+

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

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

{type}

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

Recent Activities

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

+ No recent activities found +

+
+ ) : ( +
+ {recentActivitiesData?.map((activity, index) => ( + + ))} + {!viewAll && ( +
+ + How does points work? + +
+ )} +
+ )} +
+ ); +}; + +export default RecentActivities; diff --git a/frontend/src/features/UserDashboard/components/UserDashboardComponent.tsx b/frontend/src/features/UserDashboard/components/UserDashboardComponent.tsx new file mode 100644 index 00000000..176d2692 --- /dev/null +++ b/frontend/src/features/UserDashboard/components/UserDashboardComponent.tsx @@ -0,0 +1,17 @@ +import Leaderboard from "@/features/UserDashboard/components/Leaderboard"; +import RecentActivities from "@/features/UserDashboard/components/RecentActivities"; +import Overview from "@/features/UserDashboard/components/Overview"; + +const UserDashboardComponent = () => { + return ( +
+ +
+ + +
+
+ ); +}; + +export default UserDashboardComponent; diff --git a/frontend/src/features/UserDashboard/index.tsx b/frontend/src/features/UserDashboard/index.tsx new file mode 100644 index 00000000..dcc63321 --- /dev/null +++ b/frontend/src/features/UserDashboard/index.tsx @@ -0,0 +1,12 @@ +import UserDashboardLayout from "@/shared/layout/UserDashboardLayout"; +import UserDashboardComponent from "@/features/UserDashboard/components/UserDashboardComponent"; + +const UserDashboard = () => { + return ( + + + + ); +}; + +export default UserDashboard; diff --git a/frontend/src/index.css b/frontend/src/index.css 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 ( +
+
+
+
+ Profile +
+
+ +
+

{user?.githubUsername || "username"}

+ + + + +
+
+ +
+
+ + {user?.currentBalance || "0"} +
+
+ + +
+
+
+ ); +}; + +export default UserProfileDetails; diff --git a/frontend/src/shared/components/common/ActivityCard.tsx b/frontend/src/shared/components/common/ActivityCard.tsx new file mode 100644 index 00000000..5d55a755 --- /dev/null +++ b/frontend/src/shared/components/common/ActivityCard.tsx @@ -0,0 +1,58 @@ +import type { FC } from "react"; +import Coin from "@/shared/components/common/Coin"; + +interface ActivityCardProps { + contributionType: string; + repositoryName: string; + contributedAt: string; + balanceChange: number; + showLine: boolean; +} + +const ActivityCard: FC = ({ + contributionType, + repositoryName, + contributedAt, + balanceChange, + showLine = true +}) => { + return ( +
+ {showLine && ( +
+ )} + +
+ +
+
+
+ {contributionType} +
+
+ Contributed to{" "} + + <{repositoryName}> + +
+
+ Contributed on {contributedAt} +
+
+ + {balanceChange && ( +
+ + {balanceChange < 0 ? `-${Math.abs(balanceChange)}` : balanceChange} +
+ )} +
+
+ ); +}; + +export default ActivityCard; diff --git a/frontend/src/shared/components/common/Coin.tsx b/frontend/src/shared/components/common/Coin.tsx new file mode 100644 index 00000000..e85fdbdf --- /dev/null +++ b/frontend/src/shared/components/common/Coin.tsx @@ -0,0 +1,9 @@ +const Coin = () => { + return ( +
+
+
+ ); +}; + +export default Coin; diff --git a/frontend/src/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 ( +
+ +
+ +
+ {children} +
+
+
+ ); +}; + +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": {} +}