From 7a5e9244a722161dae228ed123b73799e36fd314 Mon Sep 17 00:00:00 2001 From: Laurent Le Houerou Date: Thu, 23 May 2024 08:18:54 +0400 Subject: [PATCH] wip --- .cursorrules | 0 .dockerignore | 4 + Dockerfile | 33 + Makefile | 38 - build.ps1 | 7 + cmd/common/init.go | 41 +- cmd/console/root.go | 28 +- cmd/fbrefscraper/root.go | 129 + cmd/server/root.go | 362 ++ db/card_supply.go | 17 + db/client.go | 41 + db/competittion.go | 50 + db/country.go | 40 + db/fixture.go | 69 + db/game.go | 110 + db/game_player.go | 37 + db/membership.go | 17 + db/migrations/00002_index_optimizations.sql | 9 + db/migrations/00003_card_supply.sql | 14 + db/migrations/00004_club_memberships.sql | 14 + .../00005_player_activenationalteam.sql | 7 + .../00006_rename_club_membership.sql | 11 + db/migrations/00007_membership_indexes.sql | 5 + db/migrations/00008_indexes_on_games.sql | 6 + db/migrations/00009_player_search.sql | 35 + db/migrations/00010_avgs.sql | 116 + db/player.go | 366 ++ db/repository.go | 35 + db/team.go | 47 + db/zone.go | 28 + front/.eslintrc.cjs | 18 + front/.gitignore | 24 + front/.prettierrc | 13 + front/README.md | 30 + front/bun.lockb | Bin 0 -> 161296 bytes front/index.html | 13 + front/package-lock.json | 4802 +++++++++++++++++ front/package.json | 46 + front/postcss.config.js | 6 + front/public/favicon.ico | Bin 0 -> 162228 bytes front/src/App.tsx | 18 + front/src/api/api.ts | 86 + front/src/api/card.ts | 9 + front/src/api/competition.ts | 11 + front/src/api/fixture.ts | 9 + front/src/api/game.ts | 16 + front/src/api/player.ts | 17 + front/src/api/rankings.ts | 74 + front/src/api/zone.ts | 9 + front/src/components/checkbox.tsx | 51 + front/src/components/error.tsx | 13 + front/src/components/input_wrapper.tsx | 29 + front/src/components/loader.tsx | 25 + front/src/components/numeric_input.tsx | 33 + front/src/components/searchBox.tsx | 123 + front/src/components/withdatafetching.tsx | 35 + front/src/global.css | 14 + front/src/main.tsx | 12 + front/src/pages/header.tsx | 35 + front/src/pages/layout.tsx | 13 + front/src/pages/live/index.tsx | 374 ++ front/src/pages/player/club_history.tsx | 79 + .../src/pages/player/components/card_icon.tsx | 25 + .../player/components/days_selection.tsx | 117 + .../pages/player/components/filter_select.tsx | 65 + .../src/pages/player/components/scorebox.tsx | 23 + front/src/pages/player/index.tsx | 56 + front/src/pages/player/score_graph.tsx | 184 + front/src/pages/player/score_table.tsx | 363 ++ front/src/pages/player/scores.tsx | 36 + front/src/pages/player/sidebar.tsx | 57 + .../singlerankings/competition_select.tsx | 136 + .../components/progress_bar.tsx | 67 + front/src/pages/singlerankings/index.tsx | 671 +++ .../pages/singlerankings/position_select.tsx | 42 + .../src/pages/singlerankings/zone_select.tsx | 51 + front/src/pages/xpcenter/index.tsx | 205 + front/src/router/router.tsx | 50 + front/src/types/card.ts | 27 + front/src/types/competition.ts | 15 + front/src/types/country.ts | 11 + front/src/types/fixture.ts | 8 + front/src/types/game.ts | 177 + front/src/types/player.ts | 41 + front/src/types/team.ts | 15 + front/src/types/zone.ts | 4 + front/src/vite-env.d.ts | 1 + front/tailwind.config.js | 100 + front/tsconfig.json | 25 + front/tsconfig.node.json | 11 + front/vite.config.ts | 21 + go.mod | 52 +- go.sum | 174 +- model/batch.go | 809 --- model/card.go | 65 + model/card_supply.go | 15 + model/competition.go | 15 + model/competition.sql.go | 106 - model/country.go | 12 + model/country.sql.go | 80 - model/db.go | 33 - model/fixture.go | 25 + model/fixture.sql.go | 77 - model/game.go | 40 + model/game.sql.go | 8 - model/game_player.go | 61 + model/game_player.sql.go | 8 - model/game_player_score.go | 69 + model/game_player_score.sql.go | 8 - model/membership.go | 17 + model/models.go | 164 - model/player.go | 25 + model/player.sql.go | 8 - model/sql/competition.sql | 39 - model/sql/country.sql | 49 - model/sql/fixture.sql | 18 - model/sql/game.sql | 6 - model/sql/game_player.sql | 15 - model/sql/game_player_score.sql | 128 - model/sql/player.sql | 5 - model/sql/team.sql | 5 - model/sql/zone.sql | 2 - model/team.go | 14 + model/team.sql.go | 8 - model/zone.go | 6 + model/zone.sql.go | 21 - sorare_utils/game.go | 23 +- sorare_utils/game_player.go | 52 - sorare_utils/game_player_score.go | 107 +- sorare_utils/update_service.go | 598 +- sorare_utils/updater.go | 831 +++ sqlc.yml | 28 - 132 files changed, 11846 insertions(+), 2337 deletions(-) create mode 100644 .cursorrules create mode 100644 .dockerignore create mode 100644 Dockerfile delete mode 100644 Makefile create mode 100644 build.ps1 create mode 100644 cmd/fbrefscraper/root.go create mode 100644 cmd/server/root.go create mode 100644 db/card_supply.go create mode 100644 db/client.go create mode 100644 db/competittion.go create mode 100644 db/country.go create mode 100644 db/fixture.go create mode 100644 db/game.go create mode 100644 db/game_player.go create mode 100644 db/membership.go create mode 100644 db/migrations/00002_index_optimizations.sql create mode 100644 db/migrations/00003_card_supply.sql create mode 100644 db/migrations/00004_club_memberships.sql create mode 100644 db/migrations/00005_player_activenationalteam.sql create mode 100644 db/migrations/00006_rename_club_membership.sql create mode 100644 db/migrations/00007_membership_indexes.sql create mode 100644 db/migrations/00008_indexes_on_games.sql create mode 100644 db/migrations/00009_player_search.sql create mode 100644 db/migrations/00010_avgs.sql create mode 100644 db/player.go create mode 100644 db/repository.go create mode 100644 db/team.go create mode 100644 db/zone.go create mode 100644 front/.eslintrc.cjs create mode 100644 front/.gitignore create mode 100644 front/.prettierrc create mode 100644 front/README.md create mode 100644 front/bun.lockb create mode 100644 front/index.html create mode 100644 front/package-lock.json create mode 100644 front/package.json create mode 100644 front/postcss.config.js create mode 100644 front/public/favicon.ico create mode 100644 front/src/App.tsx create mode 100644 front/src/api/api.ts create mode 100644 front/src/api/card.ts create mode 100644 front/src/api/competition.ts create mode 100644 front/src/api/fixture.ts create mode 100644 front/src/api/game.ts create mode 100644 front/src/api/player.ts create mode 100644 front/src/api/rankings.ts create mode 100644 front/src/api/zone.ts create mode 100644 front/src/components/checkbox.tsx create mode 100644 front/src/components/error.tsx create mode 100644 front/src/components/input_wrapper.tsx create mode 100644 front/src/components/loader.tsx create mode 100644 front/src/components/numeric_input.tsx create mode 100644 front/src/components/searchBox.tsx create mode 100644 front/src/components/withdatafetching.tsx create mode 100644 front/src/global.css create mode 100644 front/src/main.tsx create mode 100644 front/src/pages/header.tsx create mode 100644 front/src/pages/layout.tsx create mode 100644 front/src/pages/live/index.tsx create mode 100644 front/src/pages/player/club_history.tsx create mode 100644 front/src/pages/player/components/card_icon.tsx create mode 100644 front/src/pages/player/components/days_selection.tsx create mode 100644 front/src/pages/player/components/filter_select.tsx create mode 100644 front/src/pages/player/components/scorebox.tsx create mode 100644 front/src/pages/player/index.tsx create mode 100644 front/src/pages/player/score_graph.tsx create mode 100644 front/src/pages/player/score_table.tsx create mode 100644 front/src/pages/player/scores.tsx create mode 100644 front/src/pages/player/sidebar.tsx create mode 100644 front/src/pages/singlerankings/competition_select.tsx create mode 100644 front/src/pages/singlerankings/components/progress_bar.tsx create mode 100644 front/src/pages/singlerankings/index.tsx create mode 100644 front/src/pages/singlerankings/position_select.tsx create mode 100644 front/src/pages/singlerankings/zone_select.tsx create mode 100644 front/src/pages/xpcenter/index.tsx create mode 100644 front/src/router/router.tsx create mode 100644 front/src/types/card.ts create mode 100644 front/src/types/competition.ts create mode 100644 front/src/types/country.ts create mode 100644 front/src/types/fixture.ts create mode 100644 front/src/types/game.ts create mode 100644 front/src/types/player.ts create mode 100644 front/src/types/team.ts create mode 100644 front/src/types/zone.ts create mode 100644 front/src/vite-env.d.ts create mode 100644 front/tailwind.config.js create mode 100644 front/tsconfig.json create mode 100644 front/tsconfig.node.json create mode 100644 front/vite.config.ts delete mode 100644 model/batch.go create mode 100644 model/card.go create mode 100644 model/card_supply.go create mode 100644 model/competition.go delete mode 100644 model/competition.sql.go create mode 100644 model/country.go delete mode 100644 model/country.sql.go delete mode 100644 model/db.go create mode 100644 model/fixture.go delete mode 100644 model/fixture.sql.go create mode 100644 model/game.go delete mode 100644 model/game.sql.go create mode 100644 model/game_player.go delete mode 100644 model/game_player.sql.go create mode 100644 model/game_player_score.go delete mode 100644 model/game_player_score.sql.go create mode 100644 model/membership.go delete mode 100644 model/models.go create mode 100644 model/player.go delete mode 100644 model/player.sql.go delete mode 100644 model/sql/competition.sql delete mode 100644 model/sql/country.sql delete mode 100644 model/sql/fixture.sql delete mode 100644 model/sql/game.sql delete mode 100644 model/sql/game_player.sql delete mode 100644 model/sql/game_player_score.sql delete mode 100644 model/sql/player.sql delete mode 100644 model/sql/team.sql delete mode 100644 model/sql/zone.sql create mode 100644 model/team.go delete mode 100644 model/team.sql.go create mode 100644 model/zone.go delete mode 100644 model/zone.sql.go delete mode 100644 sorare_utils/game_player.go create mode 100644 sorare_utils/updater.go delete mode 100644 sqlc.yml diff --git a/.cursorrules b/.cursorrules new file mode 100644 index 0000000..e69de29 diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..58144f0 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +**/node_modules + +go.work +go.work.sum \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..219851e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,33 @@ +ARG GO_VERSION=1.22.1 + +FROM oven/bun as node-builder +WORKDIR /app +COPY front/package.json front/bun.lockb ./ +RUN bun install --frozen-lockfile +COPY front/ ./ +RUN bun run build + +FROM golang:${GO_VERSION}-alpine as go-builder +RUN apk add --update ca-certificates git tzdata +WORKDIR /app +COPY go.mod go.sum ./ +RUN go mod download +COPY ./ ./ +RUN rm -rf ./cmd/server/dist +COPY --from=node-builder /app/dist ./cmd/server/dist +RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o /server ./cmd/server/. + + +FROM busybox:glibc +RUN mkdir /app +RUN addgroup -S server && adduser -S -s /bin/false -G server server +WORKDIR /app +COPY --from=go-builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt +COPY --from=go-builder /usr/share/zoneinfo /usr/share/zoneinfo +COPY --from=go-builder /server /app/server +RUN chown -R server:server /app +USER server + +EXPOSE 8080 +ENTRYPOINT ["/app/server"] + diff --git a/Makefile b/Makefile deleted file mode 100644 index cd5a18f..0000000 --- a/Makefile +++ /dev/null @@ -1,38 +0,0 @@ -BINARY_NAME=bin/sorarebuddy - -BIN_DIR=$(shell dirname $(BINARY_NAME)) -include .env -export $(shell sed 's/=.*//' .env) - -ensure-bin-dir: - @mkdir -p $(BIN_DIR) - -build: ensure-bin-dir - @echo "Building..." - go build -o $(BINARY_NAME) cmd/console/root.go - -run: build - @echo "Running..." - $(info JWTTOKEN is $(JWTTOKEN)) - $(info JWTAUDIENCE is $(JWTAUDIENCE)) - ./$(BINARY_NAME) --jwtaudience $(JWTAUDIENCE) --jwttoken $(JWTTOKEN) --dbhost $(DBHOST) --dbport $(DBPORT) --dbuser $(DBUSER) --dbpass $(DBPASS) --dbname $(DBNAME) --verbose - -clean: - @echo "Cleaning..." - go clean - rm -f $(BINARY_NAME) - -downdb: - goose -dir ./db/migrations postgres "host=$(DBHOST) port=$(DBPORT) user=$(DBUSER) password=$(DBPASS) dbname=$(DBNAME) sslmode=disable" down - -updb: - goose -dir ./db/migrations postgres "host=$(DBHOST) port=$(DBPORT) user=$(DBUSER) password=$(DBPASS) dbname=$(DBNAME) sslmode=disable" up - -resetdb: - make downdb - make updb - -.PHONY: build run clean ensure-bin-dir resetdb downdb updb -.DEFAULT_GOAL := all -all: build run clean ensure-bin-dir - diff --git a/build.ps1 b/build.ps1 new file mode 100644 index 0000000..3f45861 --- /dev/null +++ b/build.ps1 @@ -0,0 +1,7 @@ +docker build --rm -t registry.lehouerou.net/sorareplus:latest . +if($?) +{ + docker push registry.lehouerou.net/sorareplus:latest + ssh srv03 'docker pull registry.lehouerou.net/sorareplus; cd services/sorare; docker compose up -d sorareplus' +} + diff --git a/cmd/common/init.go b/cmd/common/init.go index 888f6b1..e2f319c 100644 --- a/cmd/common/init.go +++ b/cmd/common/init.go @@ -4,19 +4,22 @@ import ( "context" "fmt" "os" + "runtime" "strings" "time" "git.lehouerou.net/laurent/sorare" "git.lehouerou.net/laurent/sorare/graphql" "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/stdlib" "github.com/pkg/errors" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "github.com/spf13/cobra" "github.com/spf13/viper" - - "git.lehouerou.net/laurent/sorarebuddy/model" + "github.com/uptrace/bun" + "github.com/uptrace/bun/dialect/pgdialect" + "github.com/uptrace/bun/extra/bundebug" ) func InitParams(cmd *cobra.Command) { @@ -116,6 +119,7 @@ func InitLog() { } func InitSorare(ctx context.Context) (*sorare.Sorare, error) { + audience := viper.GetString("jwtaudience") if audience == "" { return nil, errors.New("jwtaudience is required") @@ -150,7 +154,7 @@ func InitSorare(ctx context.Context) (*sorare.Sorare, error) { return s, nil } -func InitDb(ctx context.Context) (*model.Queries, error) { +func InitDb(ctx context.Context) (*bun.DB, error) { host := viper.GetString("dbhost") if host == "" { return nil, errors.New("dbhost is required") @@ -172,25 +176,32 @@ func InitDb(ctx context.Context) (*model.Queries, error) { return nil, errors.New("dbname is required") } - conn, err := pgx.Connect( - ctx, - fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", - host, - port, - user, - password, - dbname, - ), + maxOpenConns := 4 * runtime.GOMAXPROCS(0) + pgxconfig, err := pgx.ParseConfig( + fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=disable", user, password, host, port, dbname), ) if err != nil { - return nil, errors.Wrap(err, "connecting to database") + return nil, errors.Wrap(err, "parsing pgx config") + } + sqldb := stdlib.OpenDB(*pgxconfig) + db := bun.NewDB(sqldb, pgdialect.New()) + db.SetMaxOpenConns(maxOpenConns) + + tracedb := viper.GetBool("tracedb") + if tracedb { + db.AddQueryHook(bundebug.NewQueryHook(bundebug.WithVerbose(true))) } - queries := model.New(conn) + // Test the connection + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := db.DB.PingContext(ctx); err != nil { + return nil, errors.Wrap(err, "pinging database") + } log.Info().Msgf("connected to database %s@%s:%s/%s", user, host, port, dbname) - return queries, nil + return db, nil } type contextKey string diff --git a/cmd/console/root.go b/cmd/console/root.go index b0a0231..c406c97 100644 --- a/cmd/console/root.go +++ b/cmd/console/root.go @@ -4,9 +4,10 @@ import ( "git.lehouerou.net/laurent/sorare" "github.com/pkg/errors" "github.com/spf13/cobra" + "github.com/uptrace/bun" "git.lehouerou.net/laurent/sorarebuddy/cmd/common" - "git.lehouerou.net/laurent/sorarebuddy/model" + "git.lehouerou.net/laurent/sorarebuddy/db" "git.lehouerou.net/laurent/sorarebuddy/sorare_utils" ) @@ -35,17 +36,34 @@ func run(cmd *cobra.Command, _ []string) error { return errors.New("sorare not found in context") } - db, ok := ctx.Value(common.DbContextKey).(*model.Queries) + dbconn, ok := ctx.Value(common.DbContextKey).(*bun.DB) if !ok { return errors.New("db not found in context") } - us := sorare_utils.NewUpdateService(s, db) - err := us.InitSyncDatabase(ctx) + us := sorare_utils.NewUpdateService(s, db.NewClient(dbconn)) + err := us.UpdateLastClosedStartedAndOpenedFixtures(ctx) if err != nil { - return errors.Wrap(err, "initializing database") + return errors.Wrap(err, "syncing database for last updated fixtures") } + // err := us.SyncStartedFixture(ctx) + // if err != nil { + // return errors.Wrap(err, "syncing database") + // } + // err := us.UpdatePlayers(ctx, []string{"joshua-kimmich", "leon-goretzka"}) + // if err != nil { + // return errors.Wrap(err, "updating players") + // } + // err := us.UpdateAllPlayers(ctx) + // if err != nil { + // return errors.Wrap(err, "updating all players") + // } + // err := us.UpdateCurrentlyPlayingGames(ctx) + // if err != nil { + // return errors.Wrap(err, "initializing database") + // } + // log.Debug().Msg("start sequence completed. waiting for shutdown request") // <-ctx.Done() // log.Debug().Msg("shutdown requested") diff --git a/cmd/fbrefscraper/root.go b/cmd/fbrefscraper/root.go new file mode 100644 index 0000000..50e854d --- /dev/null +++ b/cmd/fbrefscraper/root.go @@ -0,0 +1,129 @@ +package main + +import ( + "fmt" + "net/http" + "regexp" + "strings" + "sync" + "time" + + "github.com/PuerkitoBio/goquery" + "github.com/gocolly/colly" + "github.com/hashicorp/go-retryablehttp" + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + "github.com/samber/lo" + "github.com/spf13/cobra" + "github.com/uptrace/bun" + + "git.lehouerou.net/laurent/sorarebuddy/cmd/common" + "git.lehouerou.net/laurent/sorarebuddy/db" +) + +var Cmd = &cobra.Command{ + Use: "console", + Short: "console", + Long: `console`, + RunE: run, + PersistentPreRunE: common.CmdPreRunE, +} + +func main() { + common.Start(Cmd) +} + +func init() { + common.InitParams(Cmd) +} + +func run(cmd *cobra.Command, _ []string) error { + + httpClient := retryablehttp.NewClient() + + ctx := cmd.Context() + dbconn, ok := ctx.Value(common.DbContextKey).(*bun.DB) + if !ok { + return errors.New("db not found in context") + } + dbclient := db.NewClient(dbconn) + + players, err := dbclient.Players.GetAll(ctx) + if err != nil { + return errors.Wrap(err, "") + } + for _, p := range lo.Chunk(players, 100)[0] { + name := strings.ReplaceAll(p.DisplayName, " ", "+") + _, url, err := goQueryFromURL(httpClient.StandardClient(), + fmt.Sprintf("https://fbref.com/fr/search/search.fcgi?hint=&search=%s", name), + ) + if err != nil { + return errors.Wrap(err, "") + } + log.Debug().Msgf("%s -> %s", p.DisplayName, url) + } + + return nil + +} + +func goQueryFromURL(httpClient *http.Client, url string) (*goquery.Document, string, error) { + res, err := httpClient.Get(url) + if err != nil { + return nil, "", errors.Wrap(err, "requesting url") + } + defer res.Body.Close() + log.Debug().Int("status", res.StatusCode).Str("url", url).Msg("HTTP request completed") + if res.StatusCode < 200 || res.StatusCode >= 300 { + return nil, "", errors.Wrapf(err, "requesting failed at the http level: %d %s", res.StatusCode, res.Status) + } + doc, err := goquery.NewDocumentFromReader(res.Body) + if err != nil { + return nil, "", errors.Wrap(err, "parsing html") + } + return doc, res.Request.URL.String(), nil +} + +func scrapePlayers() { + c := colly.NewCollector( + colly.AllowedDomains("fbref.com"), + colly.Async(true), + ) + + players := make(map[string]bool) + mutex := &sync.Mutex{} + + c.Limit(&colly.LimitRule{ + DomainGlob: "fbref.com", + Parallelism: 2, + Delay: 1 * time.Second, + }) + + c.OnHTML("a[href]", func(e *colly.HTMLElement) { + link := e.Attr("href") + if matched, _ := regexp.MatchString(`^/fr/joueurs/[a-z0-9]{8}/[a-zA-Z-]+$`, link); matched { + splitLink := strings.Split(link, "/") + playerName := splitLink[len(splitLink)-1] + mutex.Lock() + if _, ok := players[playerName]; !ok { + players[playerName] = true + mutex.Unlock() + link = strings.Join(splitLink[:len(splitLink)-1], "/") + "/scout/365_m2/Rapport-de-scouting-" + playerName + c.Visit(e.Request.AbsoluteURL(link)) + } else { + mutex.Unlock() + } + } + }) + + c.OnRequest(func(r *colly.Request) { + log.Debug().Str("url", r.URL.String()).Msg("") + }) + + c.Visit("https://fbref.com/fr/joueurs/df69b544/Antoine-Griezmann") + c.Wait() + + for playerName := range players { + log.Debug().Str("player_name", playerName).Msg("") + } +} diff --git a/cmd/server/root.go b/cmd/server/root.go new file mode 100644 index 0000000..dd2bfdd --- /dev/null +++ b/cmd/server/root.go @@ -0,0 +1,362 @@ +package main + +import ( + "context" + "embed" + "fmt" + "net/http" + "strconv" + "strings" + "sync" + "time" + + "git.lehouerou.net/laurent/sorare" + "git.lehouerou.net/laurent/sorare/football" + "git.lehouerou.net/laurent/sorare/types" + "github.com/dustin/go-humanize" + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" + "github.com/pkg/errors" + "github.com/robfig/cron/v3" + "github.com/rs/zerolog/log" + "github.com/samber/lo" + "github.com/sourcegraph/conc" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "github.com/uptrace/bun" + + "git.lehouerou.net/laurent/sorarebuddy/cmd/common" + "git.lehouerou.net/laurent/sorarebuddy/db" + "git.lehouerou.net/laurent/sorarebuddy/model" + "git.lehouerou.net/laurent/sorarebuddy/sorare_utils" +) + +var ( + //go:embed all:dist + dist embed.FS + + //go:embed dist/index.html + indexHTML embed.FS + distDirFS = echo.MustSubFS(dist, "dist/assets") + distIndexHtml = echo.MustSubFS(indexHTML, "dist") +) + +var Cmd = &cobra.Command{ + Use: "console", + Short: "console", + Long: `console`, + RunE: run, + PersistentPreRunE: common.CmdPreRunE, +} + +func main() { + common.Start(Cmd) +} + +func init() { + common.InitParams(Cmd) + + Cmd.PersistentFlags().Bool("devmode", false, "Dev mode") + _ = viper.BindPFlag("devmode", Cmd.PersistentFlags().Lookup("devmode")) + viper.SetDefault("devmode", false) +} + +func errorResponse(err error) map[string]interface{} { + return map[string]interface{}{"error": err.Error()} +} + +func run(cmd *cobra.Command, _ []string) error { + var wg conc.WaitGroup + + ctx := cmd.Context() + dbconn, ok := ctx.Value(common.DbContextKey).(*bun.DB) + if !ok { + return errors.New("db not found in context") + } + + sorareClient, ok := ctx.Value(common.SorareContextKey).(*sorare.Sorare) + if !ok { + return errors.New("sorare not found in context") + } + + devmode := viper.GetBool("devmode") + + dbclient := db.NewClient(dbconn) + + e := echo.New() + e.FileFS("/*", "index.html", distIndexHtml) + e.StaticFS("/assets", distDirFS) + + e.Use(middleware.Recover()) + e.HTTPErrorHandler = func(err error, c echo.Context) { + statusCode := http.StatusInternalServerError + if he, ok := err.(*echo.HTTPError); ok { + statusCode = he.Code + } + + log.Error(). + Err(err). + Str("method", c.Request().Method). + Str("url", c.Request().URL.String()). + Int("status", statusCode). + Msg("HTTP error encountered") + + if statusCode == http.StatusInternalServerError { + if err := c.JSON(statusCode, errorResponse(errors.New("internal server error"))); err != nil { + log.Error().Err(err).Msg("error while sending error response") + } + } else { + if err := c.JSON(statusCode, errorResponse(err)); err != nil { + log.Error().Err(err).Msg("error while sending error response") + } + } + } + e.Use(func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + start := time.Now() + err := next(c) + latency := time.Since(start) + + latencyDisplay := fmt.Sprintf("%dms", latency.Milliseconds()) + if latency > time.Second { + latencyDisplay = fmt.Sprintf("%.2fs", latency.Seconds()) + } + clientIP := c.RealIP() + + log.Info().Msgf( + "%d %4s | %s | %6s | <- %6s | -> %6s | %s", + c.Response().Status, + c.Request().Method, + clientIP, + latencyDisplay, + humanize.Bytes(uint64(c.Request().ContentLength)), + humanize.Bytes(uint64(c.Response().Size)), + c.Request().RequestURI, + ) + + return err + } + }) + e.Use(middleware.GzipWithConfig(middleware.GzipConfig{ + Level: 5, + })) + e.Use(middleware.CORSWithConfig(middleware.CORSConfig{ + AllowOrigins: []string{"https://www.sorareplus.xyz", "http://localhost:5173"}, + })) + e.Use(middleware.SecureWithConfig(middleware.SecureConfig{ + XFrameOptions: "DENY", + ContentTypeNosniff: "nosniff", + XSSProtection: "1; mode=block", + ContentSecurityPolicy: "default-src 'self'; img-src 'self' https://assets.sorare.com https://frontend-assets.sorare.com; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'", + HSTSMaxAge: 31536000, + ReferrerPolicy: "no-referrer", + })) + + s := &ApiServer{ + sorare: sorareClient, + db: dbclient, + e: e, + } + + api := e.Group("api") + + api.POST("/search/multi", s.SearchMulti) + + fixture := api.Group("/fixture") + fixture.GET("/all", s.GetAllFixtures) + fixture.GET("/:slug/games", s.GetFixtureGames) + + player := api.Group("/player") + player.GET("/:slug", s.GetPlayer) + player.GET("/:slug/games", s.GetPlayerGames) + api.GET("/zone/all", s.GetAllZones) + api.GET("/competition/club", s.GetAllClubCompetitions) + api.POST("/rankings/single", s.SingleRankings) + api.GET("/user/:slug/cards", s.GetUserCards) + + wg.Go(func() { + defer func() { + log.Debug().Msg("http server stopped") + }() + err := e.Start(":8080") + if err != nil && !errors.Is(err, http.ErrServerClosed) { + log.Error().Err(err).Msg("running api http server") + } + }) + + c := cron.New() + if !devmode { + _, err := c.AddFunc("0 6 * * *", func() { + us := sorare_utils.NewUpdateService(sorareClient, db.NewClient(dbconn)) + err := us.UpdateLastClosedStartedAndOpenedFixtures(ctx) + if err != nil { + log.Error().Err(err).Msg("syncing database") + } + }) + if err != nil { + return errors.Wrap(err, "adding database sync job") + } + + var updateMutex sync.Mutex + _, err = c.AddFunc("*/1 * * * *", func() { + if updateMutex.TryLock() { + defer updateMutex.Unlock() + us := sorare_utils.NewUpdateService(sorareClient, db.NewClient(dbconn)) + err := us.UpdateCurrentlyPlayingGames(ctx) + if err != nil { + log.Error().Err(err).Msg("syncing database") + } + } else { + log.Info().Msg("Previous update still running, skipping this cycle.") + } + }) + if err != nil { + return errors.Wrap(err, "adding database sync job") + } + } + c.Start() + + log.Info().Msgf("%d scheduled jobs", len(c.Entries())) + for _, entry := range c.Entries() { + log.Info().Msgf("next @ %s", entry.Next) + } + + <-ctx.Done() + c.Stop() + ctxhttp, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + err := e.Shutdown(ctxhttp) + if err != nil { + log.Error().Err(err).Msg("shutting down api http server") + } + + wg.Wait() + return nil +} + +type ApiServer struct { + sorare *sorare.Sorare + db *db.Client + e *echo.Echo +} + +func (s *ApiServer) GetAllZones(c echo.Context) error { + ctx := c.Request().Context() + res, err := s.db.Zones.GetAll(ctx) + if err != nil { + return err + } + return c.JSON(http.StatusOK, res) +} + +func (s *ApiServer) GetPlayer(c echo.Context) error { + ctx := c.Request().Context() + slug := c.Param("slug") + res, err := s.db.Players.GetOne(ctx, slug) + if err != nil { + return err + } + return c.JSON(http.StatusOK, res) +} + +func (s *ApiServer) SingleRankings(c echo.Context) error { + ctx := c.Request().Context() + var opts db.SingleRankingsOptions + if err := c.Bind(&opts); err != nil { + return errors.Wrap(err, "parsing ranking request options") + } + res, err := s.db.Players.SingleRankings(ctx, opts) + if err != nil { + return errors.Wrap(err, "getting rankings") + } + return c.JSON(http.StatusOK, res) +} + +func (s *ApiServer) GetAllClubCompetitions(c echo.Context) error { + ctx := c.Request().Context() + zones := c.QueryParam("zones") + zoneArray := lo.Filter(strings.Split(zones, ","), func(s string, _ int) bool { + return s != "" + }) + res, err := s.db.Competitions.GetAllClubCompetitions(ctx, lo.Map(zoneArray, func(s string, _ int) int { + return lo.Must(strconv.Atoi(s)) + })) + if err != nil { + return err + } + return c.JSON(http.StatusOK, res) +} + +func (s *ApiServer) GetAllFixtures(c echo.Context) error { + ctx := c.Request().Context() + res, err := s.db.Fixtures.GetAll(ctx) + if err != nil { + return err + } + return c.JSON(http.StatusOK, res) +} + +func (s *ApiServer) GetFixtureGames(c echo.Context) error { + ctx := c.Request().Context() + slug := c.Param("slug") + res, err := s.db.Games.GetByFixture(ctx, slug) + if err != nil { + return err + } + return c.JSON(http.StatusOK, res) +} + +func (s *ApiServer) GetUserCards(c echo.Context) error { + ctx := c.Request().Context() + slug := c.Param("slug") + res, err := s.sorare.Users.FootballCards(slug).Get(ctx, football.CardsParams{ + Rarities: []types.Rarity{types.RarityLimited, types.RarityRare, types.RaritySuperRare, types.RarityUnique}, + }) + if err != nil { + return err + } + return c.JSON(http.StatusOK, lo.Map(res, func(card football.Card, _ int) model.Card { + return model.NewCardFromSorare(card) + })) +} + +func (s *ApiServer) GetPlayerGames(c echo.Context) error { + ctx := c.Request().Context() + slug := c.Param("slug") + res, err := s.db.GamePlayers.GetByPlayer(ctx, slug) + if err != nil { + return err + } + return c.JSON(http.StatusOK, res) +} + +func (s *ApiServer) SearchMulti(c echo.Context) error { + ctx := c.Request().Context() + + var searchText struct { + Query string `json:"query"` + Limit int `json:"limit"` + } + if err := c.Bind(&searchText); err != nil { + return c.JSON(http.StatusBadRequest, errorResponse(err)) + } + + var res struct { + Players []model.Player `json:"players"` + Teams []model.Team `json:"teams"` + } + + players, err := s.db.Players.SearchByDisplayName(ctx, searchText.Query, searchText.Limit) + if err != nil { + return err + } + res.Players = players + + teams, err := s.db.Teams.SearchByDisplayName(ctx, searchText.Query, searchText.Limit) + if err != nil { + return err + } + res.Teams = teams + + return c.JSON(http.StatusOK, res) +} diff --git a/db/card_supply.go b/db/card_supply.go new file mode 100644 index 0000000..bec2da9 --- /dev/null +++ b/db/card_supply.go @@ -0,0 +1,17 @@ +package db + +import ( + "github.com/uptrace/bun" + + "git.lehouerou.net/laurent/sorarebuddy/model" +) + +type CardSupplyRepository struct { + *Repository[model.CardSupply] +} + +func NewCardSupplyRepository(db *bun.DB) *CardSupplyRepository { + return &CardSupplyRepository{ + Repository: NewRepository[model.CardSupply](db, []string{"player_slug", "season_start_year"}), + } +} diff --git a/db/client.go b/db/client.go new file mode 100644 index 0000000..3ac2f8b --- /dev/null +++ b/db/client.go @@ -0,0 +1,41 @@ +package db + +import ( + "github.com/uptrace/bun" + + "git.lehouerou.net/laurent/sorarebuddy/model" +) + +type Client struct { + db *bun.DB + + CardSupplies *CardSupplyRepository + Memberships *MembershipRepository + Countries *CountryRepository + Fixtures *FixtureRepository + Competitions *CompetitionRepository + Teams *TeamRepository + Games *GameRepository + Players *PlayerRepository + GamePlayers *GamePlayerRepository + GamePlayerScores *Repository[model.GamePlayerScore] + Zones *ZoneRepository +} + +func NewClient(db *bun.DB) *Client { + return &Client{ + db: db, + CardSupplies: NewCardSupplyRepository(db), + Memberships: NewMembershipRepository(db), + Countries: NewCountryRepository(db), + Fixtures: NewFixtureRepository(db), + Competitions: NewCompetitionRepository(db), + Teams: NewTeamRepository(db), + Games: NewGameRepository(db), + Players: NewPlayerRepository(db), + GamePlayers: NewGamePlayerRepository(db), + GamePlayerScores: NewRepository[model.GamePlayerScore](db, []string{"game_id", "player_slug"}), + Zones: NewZoneRepository(db), + } + +} diff --git a/db/competittion.go b/db/competittion.go new file mode 100644 index 0000000..19f3838 --- /dev/null +++ b/db/competittion.go @@ -0,0 +1,50 @@ +package db + +import ( + "context" + + "github.com/samber/lo" + "github.com/uptrace/bun" + + "git.lehouerou.net/laurent/sorarebuddy/model" +) + +type CompetitionRepository struct { + *Repository[model.Competition] +} + +func NewCompetitionRepository(db *bun.DB) *CompetitionRepository { + return &CompetitionRepository{ + Repository: NewRepository[model.Competition](db, []string{"slug"}), + } +} + +func (r *CompetitionRepository) GetAllClubCompetitions(ctx context.Context, zones []int) ([]model.Competition, error) { + var res []model.Competition + request := r.db.NewSelect().Model(&res). + Where("competition_type = ?", "CLUB"). + Relation("Zone") + if len(zones) > 0 { + request = request.Where("zone_id IN (?)", bun.In(zones)) + } + if err := request.Scan(ctx); err != nil { + return nil, err + } + return res, nil +} + +func (r *CompetitionRepository) GetCompetitionSlugsNotInDb( + ctx context.Context, + competitionSlugs []string, +) ([]string, error) { + var competitions []model.Competition + err := r.db.NewSelect().Model(&competitions).Where("slug IN (?)", bun.In(competitionSlugs)).Scan(ctx) + if err != nil { + return nil, err + } + diff, _ := lo.Difference( + competitionSlugs, + lo.Map(competitions, func(c model.Competition, index int) string { return c.Slug }), + ) + return diff, nil +} diff --git a/db/country.go b/db/country.go new file mode 100644 index 0000000..c6c5e88 --- /dev/null +++ b/db/country.go @@ -0,0 +1,40 @@ +package db + +import ( + "context" + + "github.com/pkg/errors" + "github.com/samber/lo" + "github.com/uptrace/bun" + + "git.lehouerou.net/laurent/sorarebuddy/model" +) + +type CountryRepository struct { + *Repository[model.Country] +} + +func NewCountryRepository(db *bun.DB) *CountryRepository { + return &CountryRepository{ + Repository: NewRepository[model.Country](db, []string{"slug"}), + } +} + +func (r *CountryRepository) GetBySlug(ctx context.Context, slug string) (model.Country, error) { + var res model.Country + err := r.db.NewSelect().Model(&res).Where("slug = ?", slug).Scan(ctx) + if err != nil { + return model.Country{}, errors.Wrapf(err, "getting country by slug : %s", slug) + } + return res, nil +} + +func (r *CountryRepository) GetCountrySlugsNotInDb(ctx context.Context, countrySlugs []string) ([]string, error) { + var countries []model.Country + err := r.db.NewSelect().Model(&countries).Where("slug IN (?)", bun.In(countrySlugs)).Scan(ctx) + if err != nil { + return nil, err + } + diff, _ := lo.Difference(countrySlugs, lo.Map(countries, func(c model.Country, index int) string { return c.Slug })) + return diff, nil +} diff --git a/db/fixture.go b/db/fixture.go new file mode 100644 index 0000000..e6e8a5b --- /dev/null +++ b/db/fixture.go @@ -0,0 +1,69 @@ +package db + +import ( + "context" + + "github.com/uptrace/bun" + + "git.lehouerou.net/laurent/sorarebuddy/model" +) + +type FixtureRepository struct { + *Repository[model.Fixture] +} + +func NewFixtureRepository(db *bun.DB) *FixtureRepository { + return &FixtureRepository{ + Repository: NewRepository[model.Fixture](db, []string{"slug"}), + } +} + +func (r *FixtureRepository) GetByGameWeek(ctx context.Context, gameweeks ...int) ([]model.Fixture, error) { + var fixtures []model.Fixture + err := r.db.NewSelect(). + Model(&fixtures). + Where("game_week IN (?)", bun.In(gameweeks)). + Scan(ctx) + + return fixtures, err +} + +func (r *FixtureRepository) GetBySlug(ctx context.Context, slug string) (model.Fixture, error) { + var fixture model.Fixture + err := r.db.NewSelect(). + Model(&fixture). + Where("slug = ?", slug). + Scan(ctx) + + return fixture, err +} + +func (r *FixtureRepository) GetStarted(ctx context.Context) ([]model.Fixture, error) { + var fixtures []model.Fixture + err := r.db.NewSelect(). + Model(&fixtures). + Where("state = ?", "started"). + Scan(ctx) + return fixtures, err +} + +func (r *FixtureRepository) GetOpened(ctx context.Context) ([]model.Fixture, error) { + var fixtures []model.Fixture + err := r.db.NewSelect(). + Model(&fixtures). + Where("state = ?", "opened"). + Scan(ctx) + return fixtures, err +} + +func (r *FixtureRepository) GetLastClosed(ctx context.Context) (model.Fixture, error) { + var fixture model.Fixture + err := r.db.NewSelect(). + Model(&fixture). + Where("state IN (?)", bun.In([]string{"closed", "computed"})). + Order("game_week DESC"). + Limit(1). + Scan(ctx) + + return fixture, err +} diff --git a/db/game.go b/db/game.go new file mode 100644 index 0000000..28e7370 --- /dev/null +++ b/db/game.go @@ -0,0 +1,110 @@ +package db + +import ( + "context" + "time" + + "github.com/uptrace/bun" + + "git.lehouerou.net/laurent/sorarebuddy/model" +) + +type GameRepository struct { + *Repository[model.Game] +} + +func NewGameRepository(db *bun.DB) *GameRepository { + return &GameRepository{ + Repository: NewRepository[model.Game](db, []string{"id"}), + } +} + +func (r *GameRepository) GetByFixture(ctx context.Context, fixtureSlug string) ([]model.Game, error) { + var games []model.Game + err := r.db.NewSelect(). + Model(&games). + Relation("HomeTeam"). + Relation("AwayTeam"). + Relation("WinnerTeam"). + Relation("Competition"). + Relation("Competition.Country"). + Relation("Competition.Zone"). + Relation("GamePlayers"). + Relation("GamePlayers.Player"). + Relation("GamePlayers.Score"). + Where("fixture_slug = ?", fixtureSlug). + Scan(ctx) + return games, err +} + +func (r *GameRepository) GetByIds(ctx context.Context, ids []string) ([]model.Game, error) { + var games []model.Game + err := r.db.NewSelect(). + Model(&games). + Relation("HomeTeam"). + Relation("AwayTeam"). + Relation("WinnerTeam"). + Relation("Competition"). + Relation("Competition.Country"). + Relation("Competition.Zone"). + Relation("GamePlayers"). + Relation("GamePlayers.Player"). + Relation("GamePlayers.Score"). + Where("id IN (?)", bun.In(ids)). + Scan(ctx) + return games, err +} + +func (r *GameRepository) CurrentlyPlayingGames(ctx context.Context) ([]model.Game, error) { + var games []model.Game + if err := r.db.NewSelect(). + Model(&games). + Relation("HomeTeam"). + Relation("AwayTeam"). + Relation("WinnerTeam"). + Relation("Competition"). + Relation("Competition.Country"). + Relation("Competition.Zone"). + Relation("GamePlayers"). + Relation("GamePlayers.Player"). + Relation("GamePlayers.Score"). + Join("INNER JOIN fixtures AS f ON f.slug = game.fixture_slug"). + Where("f.state = 'started' AND (game.status = 'playing' OR (game.status = 'played' AND game.date > now() - interval '3 hours') OR (game.status = 'scheduled' AND game.date < now() + interval '1 hour'))"). + OrderExpr("game.date ASC"). + Scan(ctx); err != nil { + return nil, err + } + return games, nil +} + +func (r *GameRepository) GetFutureGameIdsWithoutFormation( + ctx context.Context, + startingIn time.Duration, + minPlayers int, +) ([]string, error) { + var gameIds []string + subQueryHome := r.db.NewSelect(). + ColumnExpr("COUNT(*)"). + Model((*model.GamePlayer)(nil)). + Where("game_id = game.id"). + Where("team_slug = game.home_team_slug") + + subQueryAway := r.db.NewSelect(). + ColumnExpr("COUNT(*)"). + Model((*model.GamePlayer)(nil)). + Where("game_id = game.id"). + Where("team_slug = game.away_team_slug") + + if err := r.db.NewSelect(). + Model((*model.Game)(nil)). + Column("id"). + Where("date > ?", time.Now().Add(-3*time.Hour)). + Where("date < ?", time.Now().Add(startingIn)). + Where(`(?) < ? OR (?) < ?`, + subQueryHome, minPlayers, + subQueryAway, minPlayers). + Scan(ctx, &gameIds); err != nil { + return nil, err + } + return gameIds, nil +} diff --git a/db/game_player.go b/db/game_player.go new file mode 100644 index 0000000..e0f6639 --- /dev/null +++ b/db/game_player.go @@ -0,0 +1,37 @@ +package db + +import ( + "context" + + "github.com/uptrace/bun" + + "git.lehouerou.net/laurent/sorarebuddy/model" +) + +type GamePlayerRepository struct { + *Repository[model.GamePlayer] +} + +func NewGamePlayerRepository(db *bun.DB) *GamePlayerRepository { + return &GamePlayerRepository{ + Repository: NewRepository[model.GamePlayer](db, []string{"game_id", "player_slug"}), + } +} + +func (r *GamePlayerRepository) GetByPlayer(ctx context.Context, playerSlug string) ([]model.GamePlayer, error) { + var res []model.GamePlayer + err := r.db.NewSelect(). + Model(&res). + Relation("Game"). + Relation("Game.HomeTeam"). + Relation("Game.AwayTeam"). + Relation("Game.WinnerTeam"). + Relation("Game.Fixture"). + Relation("Score"). + Where("game_player.player_slug = ?", playerSlug). + Scan(ctx) + if err != nil { + return nil, err + } + return res, nil +} diff --git a/db/membership.go b/db/membership.go new file mode 100644 index 0000000..110c8c8 --- /dev/null +++ b/db/membership.go @@ -0,0 +1,17 @@ +package db + +import ( + "github.com/uptrace/bun" + + "git.lehouerou.net/laurent/sorarebuddy/model" +) + +type MembershipRepository struct { + *Repository[model.Membership] +} + +func NewMembershipRepository(db *bun.DB) *MembershipRepository { + return &MembershipRepository{ + Repository: NewRepository[model.Membership](db, []string{"id"}), + } +} diff --git a/db/migrations/00002_index_optimizations.sql b/db/migrations/00002_index_optimizations.sql new file mode 100644 index 0000000..7b48b68 --- /dev/null +++ b/db/migrations/00002_index_optimizations.sql @@ -0,0 +1,9 @@ +-- +goose Up +CREATE INDEX IF NOT EXISTS idx_games_date ON public.games(date); +CREATE INDEX IF NOT EXISTS idx_competitions_zone_id ON public.competitions(zone_id); +CREATE INDEX IF NOT EXISTS idx_player_field_position ON public.players(field_position); + +-- +goose Down +DROP INDEX IF EXISTS idx_games_date; +DROP INDEX IF EXISTS idx_competitions_zone_id; +DROP INDEX IF EXISTS idx_player_field_position; diff --git a/db/migrations/00003_card_supply.sql b/db/migrations/00003_card_supply.sql new file mode 100644 index 0000000..1ad9157 --- /dev/null +++ b/db/migrations/00003_card_supply.sql @@ -0,0 +1,14 @@ +-- +goose Up + CREATE TABLE IF NOT EXISTS card_supplies ( + player_slug TEXT NOT NULL, + season_start_year INTEGER NOT NULL, + PRIMARY KEY (player_slug, season_start_year), + "limited" INTEGER NOT NULL, + "rare" INTEGER NOT NULL, + "super_rare" INTEGER NOT NULL, + "unique" INTEGER NOT NULL, + "last_updated" TIMESTAMPTZ NOT NULL + ); + +-- +goose Down + DROP TABLE IF EXISTS card_supplies; \ No newline at end of file diff --git a/db/migrations/00004_club_memberships.sql b/db/migrations/00004_club_memberships.sql new file mode 100644 index 0000000..7e31442 --- /dev/null +++ b/db/migrations/00004_club_memberships.sql @@ -0,0 +1,14 @@ +-- +goose Up +CREATE TABLE IF NOT EXISTS club_memberships( + id TEXT PRIMARY KEY, + player_slug TEXT NOT NULL REFERENCES players(slug), + club_slug TEXT NOT NULL REFERENCES teams(slug), + start_date DATE NOT NULL, + end_date DATE +); + +CREATE INDEX IF NOT EXISTS club_memberships_player_slug_start_date_end_date_idx ON club_memberships(player_slug, start_date, end_date); + +-- +goose Down +DROP INDEX IF EXISTS club_memberships_player_slug_start_date_end_date_idx; +DROP TABLE IF EXISTS club_memberships; diff --git a/db/migrations/00005_player_activenationalteam.sql b/db/migrations/00005_player_activenationalteam.sql new file mode 100644 index 0000000..2bff6a6 --- /dev/null +++ b/db/migrations/00005_player_activenationalteam.sql @@ -0,0 +1,7 @@ +-- +goose Up +ALTER TABLE players ADD COLUMN active_national_team_slug TEXT; + +-- +goose Down +ALTER TABLE players DROP COLUMN active_national_team_slug; + + diff --git a/db/migrations/00006_rename_club_membership.sql b/db/migrations/00006_rename_club_membership.sql new file mode 100644 index 0000000..e950940 --- /dev/null +++ b/db/migrations/00006_rename_club_membership.sql @@ -0,0 +1,11 @@ +-- +goose Up +ALTER TABLE club_memberships RENAME TO memberships; +ALTER TABLE memberships RENAME COLUMN club_slug TO team_slug; +ALTER TABLE memberships ADD COLUMN membership_type TEXT NOT NULL DEFAULT 'club'; + + +-- +goose Down +ALTER TABLE memberships RENAME TO club_memberships; +ALTER TABLE club_memberships RENAME COLUMN team_slug TO club_slug; +ALTER TABLE club_memberships DROP COLUMN membership_type; + diff --git a/db/migrations/00007_membership_indexes.sql b/db/migrations/00007_membership_indexes.sql new file mode 100644 index 0000000..4c7f993 --- /dev/null +++ b/db/migrations/00007_membership_indexes.sql @@ -0,0 +1,5 @@ +-- +goose Up +CREATE INDEX IF NOT EXISTS idx_memberships_on_player_slug_start_date_end_date ON memberships(player_slug, start_date, end_date); + +-- +goose Down +DROP INDEX IF EXISTS idx_memberships_on_player_slug_start_date_end_date; diff --git a/db/migrations/00008_indexes_on_games.sql b/db/migrations/00008_indexes_on_games.sql new file mode 100644 index 0000000..82d5d84 --- /dev/null +++ b/db/migrations/00008_indexes_on_games.sql @@ -0,0 +1,6 @@ +-- +goose Up +CREATE INDEX IF NOT EXISTS idx_games_away_team_slug_date ON games(away_team_slug, date); +CREATE INDEX IF NOT EXISTS idx_games_home_team_slug_date ON games(home_team_slug, date); +-- +goose Down +DROP INDEX IF EXISTS idx_games_away_team_slug_date; +DROP INDEX IF EXISTS idx_games_home_team_slug_date; diff --git a/db/migrations/00009_player_search.sql b/db/migrations/00009_player_search.sql new file mode 100644 index 0000000..a235717 --- /dev/null +++ b/db/migrations/00009_player_search.sql @@ -0,0 +1,35 @@ +-- +goose Up +CREATE EXTENSION IF NOT EXISTS pg_trgm; +CREATE EXTENSION IF NOT EXISTS unaccent; + +CREATE OR REPLACE FUNCTION immutable_unaccent(regdictionary, TEXT) + RETURNS TEXT + LANGUAGE c + IMMUTABLE PARALLEL SAFE STRICT AS +'$libdir/unaccent', +'unaccent_dict'; + +CREATE OR REPLACE FUNCTION f_unaccent(TEXT) RETURNS TEXT + immutable + strict + parallel safe + language sql +as +$$ +SELECT public.immutable_unaccent(regdictionary 'public.unaccent', $1) +$$; + +CREATE INDEX IF NOT EXISTS players_unaccent_display_name_trgm_idx + ON players USING gin (f_unaccent(display_name::text) gin_trgm_ops); + +CREATE INDEX IF NOT EXISTS teams_unaccent_display_name_trgm_idx + ON teams USING gin (f_unaccent(display_name::text) gin_trgm_ops); + +-- +goose Down +DROP INDEX IF EXISTS players_unaccent_display_name_trgm_idx; +DROP INDEX IF EXISTS teams_unaccent_display_name_trgm_idx; +DROP FUNCTION IF EXISTS f_unaccent; +DROP FUNCTION IF EXISTS immutable_unaccent; +DROP EXTENSION IF EXISTS unaccent; +DROP EXTENSION IF EXISTS pg_trgm; + diff --git a/db/migrations/00010_avgs.sql b/db/migrations/00010_avgs.sql new file mode 100644 index 0000000..45cd062 --- /dev/null +++ b/db/migrations/00010_avgs.sql @@ -0,0 +1,116 @@ +-- +goose Up + CREATE TABLE IF NOT EXISTS player_averages ( + player_slug VARCHAR NOT NULL, + fixture_slug VARCHAR NOT NULL, + PRIMARY KEY (player_slug, fixture_slug), + l5 INTEGER, + l5r INTEGER, + l15 INTEGER, + l15r INTEGER, + l40 INTEGER, + l40r INTEGER, + gameweek INTEGER, + ds5 INTEGER, + ds15 INTEGER, + ds40 INTEGER, + aa5 INTEGER, + aa15 INTEGER, + aa40 INTEGER, + minutes15 INTEGER + ); + + CREATE INDEX player_averages_player_slug_gameweek_index ON player_averages (player_slug ASC, gameweek DESC); + + CREATE OR REPLACE PROCEDURE calc_stats (IN starting_gameweek INTEGER) LANGUAGE plpgsql AS $$ + DECLARE + fixture RECORD; + player RECORD; + startdate TIMESTAMP WITH TIME ZONE; + l5 INTEGER; + l15 INTEGER; + l40 INTEGER; + l5r INTEGER; + l15r INTEGER; + l40r INTEGER; + ds5 INTEGER; + ds15 INTEGER; + ds40 INTEGER; + aa5 INTEGER; + aa15 INTEGER; + aa40 INTEGER; + minutes15 INTEGER; + + BEGIN + + FOR fixture IN + (SELECT * FROM fixtures WHERE game_week >= starting_gameweek ORDER BY game_week) + LOOP + RAISE NOTICE 'Processing fixture: %, Game week: %', fixture.slug, fixture.game_week; + + SELECT start_date FROM fixtures WHERE game_week = fixture.game_week - 1 INTO startdate; + IF startdate IS NULL THEN + startdate = fixture.start_date - INTERVAL '3 days'; + END IF; + + RAISE NOTICE 'Start date for calculations: %', startdate; + + FOR player IN + (SELECT * FROM players) + LOOP + RAISE NOTICE 'Calculating averages for player: %', player.slug; + WITH subquery AS (SELECT row_number() OVER (ORDER BY g.date DESC) AS rn, + score, + decisive_score, + all_around_score, + minutes + FROM game_player_scores AS gps + JOIN games AS g ON g.id = gps.game_id + WHERE player_slug = player.slug + AND g.date < startdate) + select coalesce(round(avg(score) filter ( where rn <= 5 )), 0), + coalesce(round(avg(score) filter ( where rn <= 15 )), 0), + coalesce(round(avg(score) filter ( where rn <= 40 )), 0), + coalesce(round(avg(score) filter ( where rn <= 5 and minutes > 0 )), 0), + coalesce(round(avg(score) filter ( where rn <= 15 and minutes > 0 )), 0), + coalesce(round(avg(score) filter ( where rn <= 40 and minutes > 0 )), 0), + coalesce(round(avg(decisive_score) filter ( where rn <= 5 and minutes > 0 )), 0), + coalesce(round(avg(decisive_score) filter ( where rn <= 15 and minutes > 0 )), 0), + coalesce(round(avg(decisive_score) filter ( where rn <= 40 and minutes > 0 )), 0), + coalesce(round(avg(all_around_score) filter ( where rn <= 5 and minutes > 0 )), 0), + coalesce(round(avg(all_around_score) filter ( where rn <= 15 and minutes > 0 )), 0), + coalesce(round(avg(all_around_score) filter ( where rn <= 40 and minutes > 0 )), 0), + coalesce(round(avg(minutes) filter ( where rn <= 15 )), 0) + + from subquery + into l5r, l15r, l40r, l5, l15, l40, ds5, ds15, ds40, aa5, aa15, aa40, minutes15; + + RAISE NOTICE 'Inserting/updating averages for player: %, Fixture: %, Game week: %, l15: %', player.slug, fixture.slug, fixture.game_week, l15; + + INSERT INTO player_averages (player_slug, fixture_slug, gameweek, l5, l5r, l15, l15r, l40, l40r, ds5, ds15, ds40, aa5, aa15, aa40, minutes15) + VALUES (player.slug, fixture.slug, fixture.game_week, l5, l5r, l15, l15r, l40, l40r, ds5, ds15, ds40, aa5, aa15, aa40, minutes15) + ON CONFLICT (player_slug, fixture_slug) DO UPDATE SET l5 = EXCLUDED.l5, + l5r = EXCLUDED.l5r, + l15 = EXCLUDED.l15, + l15r = EXCLUDED.l15r, + l40 = EXCLUDED.l40, + l40r = EXCLUDED.l40r, + ds5 = EXCLUDED.ds5, + ds15 = EXCLUDED.ds15, + ds40 = EXCLUDED.ds40, + aa5 = EXCLUDED.aa5, + aa15 = EXCLUDED.aa15, + aa40 = EXCLUDED.aa40, + minutes15 = EXCLUDED.minutes15; + + END LOOP; + COMMIT; + END LOOP; + +END; +$$; + +-- +goose Down +DROP PROCEDURE IF EXISTS calc_stats; +DROP INDEX IF EXISTS player_averages_player_slug_gameweek_index; +DROP TABLE IF EXISTS player_averages; + diff --git a/db/player.go b/db/player.go new file mode 100644 index 0000000..03d1077 --- /dev/null +++ b/db/player.go @@ -0,0 +1,366 @@ +package db + +import ( + "context" + "time" + + "git.lehouerou.net/laurent/sorare/types" + "github.com/pkg/errors" + "github.com/samber/lo" + "github.com/shopspring/decimal" + "github.com/uptrace/bun" + + "git.lehouerou.net/laurent/sorarebuddy/model" +) + +type PlayerRepository struct { + *Repository[model.Player] +} + +func NewPlayerRepository(db *bun.DB) *PlayerRepository { + return &PlayerRepository{ + Repository: NewRepository[model.Player](db, []string{"slug"}), + } +} + +func (r *PlayerRepository) SearchByDisplayName( + ctx context.Context, + displayName string, + limit int, +) ([]model.Player, error) { + var players []model.Player + err := r.db.NewSelect(). + Model(&players). + Relation("Team"). + Relation("DomesticLeague"). + Relation("DomesticLeague.Zone"). + Where("f_unaccent(player.display_name) ILIKE ?", "%"+displayName+"%"). + Limit(limit). + Scan(ctx) + return players, err +} + +func (r *PlayerRepository) GetOne(ctx context.Context, slug string) (model.Player, error) { + var player model.Player + err := r.db.NewSelect(). + Model(&player). + Where("player.slug = ?", slug). + Relation("CardSupply"). + Relation("ClubMembership"). + Relation("ClubMembership.Team"). + Relation("ClubMembership.Team.Country"). + Relation("ClubMembership.Team.DomesticLeague"). + Relation("ClubMembership.Team.DomesticLeague.Zone"). + Relation("Country"). + Relation("Team"). + Relation("DomesticLeague"). + Relation("DomesticLeague.Zone"). + Scan(ctx) + return player, err +} + +func (r *PlayerRepository) GetMany(ctx context.Context, slugs ...string) ([]model.Player, error) { + var players []model.Player + err := r.db.NewSelect(). + Model(&players). + Where("player.slug IN (?)", bun.In(slugs)). + Relation("CardSupply"). + Relation("ClubMembership"). + Relation("Country"). + Relation("Team"). + Relation("DomesticLeague"). + Relation("DomesticLeague.Zone"). + Scan(ctx) + return players, err +} + +func (r *PlayerRepository) GetAll(ctx context.Context) ([]model.Player, error) { + var players []model.Player + err := r.db.NewSelect(). + Model(&players). + Scan(ctx) + return players, err +} + +func (r *PlayerRepository) GetPlayerSlugsNotInDb(ctx context.Context, playerSlugs []string) ([]string, error) { + var players []model.Player + err := r.db.NewSelect(). + Model(&players). + Where("slug IN (?)", bun.In(playerSlugs)). + Scan(ctx) + if err != nil { + return nil, errors.Wrap(err, "getting players not in db") + } + diff, _ := lo.Difference(playerSlugs, lo.Map(players, func(p model.Player, index int) string { return p.Slug })) + return diff, nil +} + +type SingleRankingsOptions struct { + StartDate time.Time `json:"startDate"` + Position types.Position `json:"position"` + Competitions []string `json:"competitions"` + Zones []int `json:"zones"` + OnlyClubGames bool `json:"onlyClubGames"` + OnlyStarting bool `json:"onlyStarting"` + MinGameCount int `json:"minGameCount"` + MinTeamGamesPlayedPercentage int `json:"minTeamGamesPlayedPercentage"` + Order string `json:"order"` + Limit int `json:"limit"` + Rarity types.Rarity `json:"rarity"` + U23 bool `json:"u23"` + MinTotalMinutes int `json:"minTotalMinutes"` + MinAge int `json:"minAge"` + MaxAge int `json:"maxAge"` + HasGameInNextGw bool `json:"hasGameInNextGw"` +} + +type SingleRanking struct { + PlayerSlug string `bun:"player_slug" json:"playerSlug"` + TeamSlug string `bun:"team_slug" json:"teamSlug"` + AvgTeamGoalsFor decimal.Decimal `bun:"avg_team_goals_for" json:"avgTeamGoalsFor"` + AvgTeamGoalsAgainst decimal.Decimal `bun:"avg_team_goals_against" json:"avgTeamGoalsAgainst"` + TotalAvgScore decimal.Decimal `bun:"total_avg_score" json:"totalAvgScore"` + DecisiveAvgScore decimal.Decimal `bun:"decisive_avg_score" json:"decisiveAvgScore"` + AllAroundAvgScore decimal.Decimal `bun:"all_around_avg_score" json:"allAroundAvgScore"` + MinScore decimal.Decimal `bun:"min_score" json:"minScore"` + MaxScore decimal.Decimal `bun:"max_score" json:"maxScore"` + TotalStddevScore decimal.Decimal `bun:"total_stddev_score" json:"totalStddevScore"` + GameUsedInStatsCount int `bun:"game_used_in_stats_count" json:"gameUsedInStatsCount"` + GameStartedCount int `bun:"game_started_count" json:"gameStartedCount"` + GameBenchedPlayedCount int `bun:"game_benched_played_count" json:"gameBenchedPlayedCount"` + GameBenchedUnplayedCount int `bun:"game_benched_unplayed_count" json:"gameBenchedUnplayedCount"` + TeamsGameCount int `bun:"teams_game_count" json:"teamsGameCount"` + TotalPossibleMinutes int `bun:"total_possible_minutes" json:"totalPossibleMinutes"` + PercentageMinutesPlayed decimal.Decimal `bun:"percentage_minutes_played" json:"percentageMinutesPlayed"` + StackPlayPercentage decimal.Decimal `bun:"stack_play_percentage" json:"stackPlayPercentage"` + Floor decimal.Decimal `bun:"floor" json:"floor"` + MoreThan80Score int `bun:"more_than_80_score" json:"moreThan80Score"` + MoreThan70Score int `bun:"more_than_70_score" json:"moreThan70Score"` + MoreThan60Score int `bun:"more_than_60_score" json:"moreThan60Score"` + MoreThan50Score int `bun:"more_than_50_score" json:"moreThan50Score"` + DecisiveCount int `bun:"decisive_count" json:"decisiveCount"` + MoreThan40AA int `bun:"more_than_40_aa" json:"moreThan40AA"` + MoreThan30AA int `bun:"more_than_30_aa" json:"moreThan30AA"` + MoreThan20AA int `bun:"more_than_20_aa" json:"moreThan20AA"` + MoreThan10AA int `bun:"more_than_10_aa" json:"moreThan10AA"` + MoreThan80ScorePercentage decimal.Decimal `bun:"more_than_80_score_percentage" json:"moreThan80ScorePercentage"` + MoreThan70ScorePercentage decimal.Decimal `bun:"more_than_70_score_percentage" json:"moreThan70ScorePercentage"` + MoreThan60ScorePercentage decimal.Decimal `bun:"more_than_60_score_percentage" json:"moreThan60ScorePercentage"` + MoreThan50ScorePercentage decimal.Decimal `bun:"more_than_50_score_percentage" json:"moreThan50ScorePercentage"` + DecisiveCountPercentage decimal.Decimal `bun:"decisive_percentage" json:"decisivePercentage"` + MoreThan40AAPercentage decimal.Decimal `bun:"more_than_40_aa_percentage" json:"moreThan40AAPercentage"` + MoreThan30AAPercentage decimal.Decimal `bun:"more_than_30_aa_percentage" json:"moreThan30AAPercentage"` + MoreThan20AAPercentage decimal.Decimal `bun:"more_than_20_aa_percentage" json:"moreThan20AAPercentage"` + MoreThan10AAPercentage decimal.Decimal `bun:"more_than_10_aa_percentage" json:"moreThan10AAPercentage"` + + TotalMinutes decimal.Decimal `bun:"total_minutes" json:"totalMinutes"` + TotalAA decimal.Decimal `bun:"total_aa" json:"totalAA"` + AAPerMin decimal.Decimal `bun:"aa_per_min" json:"aaPerMin"` + Scores []*decimal.Decimal `bun:"scores,array" json:"scores"` + + Player model.Player `bun:"-" json:"player"` +} + +func CalculateCutoffBirthdate(age int) time.Time { + // Get the current time. + now := time.Now() + + // Subtract the age from the current year. + // Also subtract 6 months to reverse the original SQL operation. + birthYear := now.Year() - age + if now.Month() < 7 { // If before July, subtract an additional year. + birthYear-- + } + + // Construct the birth date using the calculated birth year. + // The month and day are set to the same as the current date for simplicity. + // Adjust these values as needed for your specific requirements. + birthDate := time.Date(birthYear, now.Month(), now.Day(), 0, 0, 0, 0, time.UTC) + + return birthDate +} + +func (r *PlayerRepository) SingleRankings(ctx context.Context, opts SingleRankingsOptions) ([]SingleRanking, error) { + + minAgeBirthdate := CalculateCutoffBirthdate(opts.MinAge) + maxAgeBirthdate := CalculateCutoffBirthdate(opts.MaxAge) + + // Return the list of team slugs for teams that have games in the currently opened game week. + NextGWGameCountRequest := r.db.NewSelect(). + ColumnExpr("t.slug"). + TableExpr("teams AS t"). + Join("JOIN games AS g ON g.away_team_slug = t.slug OR g.home_team_slug = t.slug"). + Join("JOIN fixtures AS f ON f.slug = g.fixture_slug"). + Where("f.state = 'opened'"). + Group("t.slug") + + FilteredGames := r.db.NewSelect(). + ColumnExpr("g.*"). + TableExpr("games AS g"). + Where("g.date >= ?", opts.StartDate) + if opts.OnlyClubGames { + FilteredGames.Join("JOIN competitions AS c ON c.slug = g.competition_slug") + FilteredGames.Where("c.competition_type = 'CLUB'") + } + + // Return for each player the sum of supplies over all seasons + PlayersSupplies := r.db.NewSelect(). + ColumnExpr("cs.player_slug"). + ColumnExpr("SUM(cs.limited) AS limited"). + ColumnExpr("SUM(cs.rare) AS rare"). + ColumnExpr("SUM(cs.super_rare) AS super_rare"). + ColumnExpr("SUM(cs.unique) AS unique"). + TableExpr("card_supplies AS cs"). + Group("player_slug") + + FilteredPlayers := r.db.NewSelect(). + ColumnExpr("p.*"). + TableExpr("players AS p"). + Join("JOIN \"Supplies\" AS s ON s.player_slug = p.slug"). + Join("JOIN competitions AS dc ON dc.slug = p.domestic_league_slug"). + Where("p.field_position = ?", opts.Position). + Where("p.birth_date >= ?", maxAgeBirthdate). + Where("p.birth_date <= ?", minAgeBirthdate). + Where("dc.zone_id IS NOT NULL") + if opts.U23 { + FilteredPlayers.Where("p.birth_date >= ?", CalculateCutoffBirthdate(23)) + } + if len(opts.Competitions) > 0 { + FilteredPlayers.Where("dc.slug IN (?)", bun.In(opts.Competitions)) + } + if len(opts.Zones) > 0 { + FilteredPlayers.Where("dc.zone_id IN (?)", bun.In(opts.Zones)) + } + if opts.HasGameInNextGw { + FilteredPlayers.Join("JOIN \"NextGWGameCount\" AS ngc ON ngc.slug = p.team_slug") + } + + PlayerPossibleGameCount := r.db.NewSelect(). + ColumnExpr("p.slug"). + ColumnExpr("COUNT(DISTINCT g.id) AS total_game_count"). + ColumnExpr("SUM(g.minutes) AS total_minutes"). + ColumnExpr("ARRAY_AGG(COALESCE(fs.score, -1) ORDER BY g.date DESC) AS scores"). + ColumnExpr("COUNT(DISTINCT CASE WHEN gp.status = 'starting' THEN gp.game_id ELSE NULL END) AS game_started_count"). + ColumnExpr("COUNT(DISTINCT CASE WHEN gp.status = 'bench' AND fs.minutes_played > 0 THEN gp.game_id ELSE NULL END) AS game_benched_played_count"). + ColumnExpr("COUNT(DISTINCT CASE WHEN gp.status = 'bench' AND fs.minutes_played = 0 THEN gp.game_id ELSE NULL END) AS game_benched_unplayed_count"). + ColumnExpr("SUM(fs.minutes_played) * 100 / SUM(g.minutes) AS percentage_minutes_played"). + TableExpr("\"FilteredPlayers\" AS p"). + Join("JOIN memberships AS m ON m.player_slug = p.slug AND m.start_date <= now() AND (m.end_date IS NULL OR m.end_date >= ?)", opts.StartDate). + Join("JOIN games AS g ON (g.away_team_slug = m.team_slug OR g.home_team_slug = m.team_slug) AND g.date >= GREATEST(m.start_date, ?) AND (g.date <= m.end_date OR m.end_date IS NULL) AND g.date < DATE(now())", opts.StartDate). + Join("LEFT JOIN game_players AS gp ON gp.player_slug = p.slug AND gp.game_id = g.id"). + Join("LEFT JOIN game_player_scores AS fs ON fs.player_slug = p.slug AND fs.game_id = g.id"). + Group("p.slug") + + if opts.OnlyClubGames { + PlayerPossibleGameCount.Where("m.membership_type = 'club'") + } + + // Return the list of games played by players filtered out with params + gamePlayersRequest := r.db.NewSelect(). + ColumnExpr("gp.player_slug"). + ColumnExpr("gp.game_id"). + ColumnExpr("gp.team_slug"). + ColumnExpr("CASE WHEN gp.team_slug = g.home_team_slug THEN g.home_goals ELSE g.away_goals END as team_goals_for"). + ColumnExpr("CASE WHEN gp.team_slug = g.home_team_slug THEN g.away_goals ELSE g.home_goals END as team_goals_against"). + ColumnExpr("CASE WHEN gp.team_slug = g.home_team_slug THEN 'H' ELSE 'A' END AS home_away"). + TableExpr("\"FilteredGames\" AS g"). + Join("JOIN game_players AS gp ON g.id = gp.game_id"). + Join("JOIN \"FilteredPlayers\" AS p ON p.slug = gp.player_slug") + + if opts.OnlyStarting { + gamePlayersRequest.Where("gp.status = 'starting'") + } + + scoresRequest := r.db.NewSelect(). + ColumnExpr("gp.player_slug"). + ColumnExpr("round(AVG(gp.team_goals_for), 2) AS avg_team_goals_for"). + ColumnExpr("round(AVG(gp.team_goals_against), 2) AS avg_team_goals_against"). + ColumnExpr("round(AVG(fs.score), 2) AS total_avg_score"). + ColumnExpr("round(AVG(fs.decisive_score), 2) AS decisive_avg_score"). + ColumnExpr("round(AVG(fs.all_around_score), 2) AS all_around_avg_score"). + ColumnExpr("min(fs.score) as min_score"). + ColumnExpr("max(fs.score) as max_score"). + ColumnExpr("COALESCE(round(stddev(fs.score),2), 0) AS total_stddev_score"). + ColumnExpr("COUNT(*) AS game_used_in_stats_count"). + ColumnExpr("COUNT(CASE WHEN fs.score > 80 THEN 1 ELSE NULL END) AS more_than_80_score"). + ColumnExpr("COUNT(CASE WHEN fs.score > 70 THEN 1 ELSE NULL END) AS more_than_70_score"). + ColumnExpr("COUNT(CASE WHEN fs.score > 60 THEN 1 ELSE NULL END) AS more_than_60_score"). + ColumnExpr("COUNT(CASE WHEN fs.score > 50 THEN 1 ELSE NULL END) AS more_than_50_score"). + ColumnExpr("COUNT(CASE WHEN fs.decisive_score > 60 THEN 1 ELSE NULL END) AS decisive_count"). + ColumnExpr("COUNT(CASE WHEN fs.all_around_score > 40 THEN 1 ELSE NULL END) AS more_than_40_aa"). + ColumnExpr("COUNT(CASE WHEN fs.all_around_score > 30 THEN 1 ELSE NULL END) AS more_than_30_aa"). + ColumnExpr("COUNT(CASE WHEN fs.all_around_score > 20 THEN 1 ELSE NULL END) AS more_than_20_aa"). + ColumnExpr("COUNT(CASE WHEN fs.all_around_score > 10 THEN 1 ELSE NULL END) AS more_than_10_aa"). + ColumnExpr("SUM(fs.minutes_played) as total_minutes"). + ColumnExpr("SUM(fs.all_around_score) as total_aa"). + ColumnExpr("COALESCE(ROUND(SUM(fs.all_around_score) / NULLIF(SUM(fs.minutes_played), 0), 3), 0) AS aa_per_min"). + TableExpr("\"FilteredGamePlayers\" AS gp"). + Join("JOIN game_player_scores AS fs ON gp.player_slug = fs.player_slug AND gp.game_id = fs.game_id"). + Group("gp.player_slug") + + filteredScoresRequest := r.db.NewSelect(). + ColumnExpr("s.*"). + ColumnExpr("ROUND(((s.game_used_in_stats_count::float / ppgc.total_game_count::float) * 100)::numeric, 2) AS stack_play_percentage"). + ColumnExpr("ROUND(((s.more_than_80_score::float / s.game_used_in_stats_count::float) * 100)::numeric, 2) AS more_than_80_score_percentage"). + ColumnExpr("ROUND(((s.more_than_70_score::float / s.game_used_in_stats_count::float) * 100)::numeric, 2) AS more_than_70_score_percentage"). + ColumnExpr("ROUND(((s.more_than_60_score::float / s.game_used_in_stats_count::float) * 100)::numeric, 2) AS more_than_60_score_percentage"). + ColumnExpr("ROUND(((s.more_than_50_score::float / s.game_used_in_stats_count::float) * 100)::numeric, 2) AS more_than_50_score_percentage"). + ColumnExpr("ROUND(((s.decisive_count::float / s.game_used_in_stats_count::float) * 100)::numeric, 2) AS decisive_percentage"). + ColumnExpr("ROUND(((s.more_than_40_aa::float / s.game_used_in_stats_count::float) * 100)::numeric, 2) AS more_than_40_aa_percentage"). + ColumnExpr("ROUND(((s.more_than_30_aa::float / s.game_used_in_stats_count::float) * 100)::numeric, 2) AS more_than_30_aa_percentage"). + ColumnExpr("ROUND(((s.more_than_20_aa::float / s.game_used_in_stats_count::float) * 100)::numeric, 2) AS more_than_20_aa_percentage"). + ColumnExpr("ROUND(((s.more_than_10_aa::float / s.game_used_in_stats_count::float) * 100)::numeric, 2) AS more_than_10_aa_percentage"). + ColumnExpr("ppgc.game_started_count"). + ColumnExpr("ppgc.game_benched_played_count"). + ColumnExpr("ppgc.game_benched_unplayed_count"). + ColumnExpr("ppgc.total_game_count AS teams_game_count"). + ColumnExpr("ppgc.scores"). + ColumnExpr("ppgc.total_minutes AS total_possible_minutes"). + ColumnExpr("ppgc.percentage_minutes_played"). + TableExpr("\"Scores\" AS s"). + Join("JOIN \"PlayerPossibleGameCount\" AS ppgc ON ppgc.slug = s.player_slug"). + Where("s.game_used_in_stats_count >= ?", opts.MinGameCount). + Where("s.total_minutes >= ?", opts.MinTotalMinutes) + + mainRequest := r.db.NewSelect(). + With("Supplies", PlayersSupplies). + With("NextGWGameCount", NextGWGameCountRequest). + With("FilteredGames", FilteredGames). + With("FilteredPlayers", FilteredPlayers). + With("PlayerPossibleGameCount", PlayerPossibleGameCount). + With("FilteredGamePlayers", gamePlayersRequest). + With("Scores", scoresRequest). + With("FilteredScores", filteredScoresRequest). + ColumnExpr("fs.*"). + TableExpr("\"FilteredScores\" AS fs"). + Where("fs.stack_play_percentage >= ?", opts.MinTeamGamesPlayedPercentage). + Order(opts.Order). + Limit(opts.Limit) + + var res []SingleRanking + err := mainRequest.Scan(ctx, &res) + if err != nil { + return nil, errors.Wrap(err, "selecting single rankings") + } + + playerSlugs := lo.Map(res, func(r SingleRanking, _ int) string { + return r.PlayerSlug + }) + if len(playerSlugs) == 0 { + return res, nil + } + players, err := r.GetMany(ctx, playerSlugs...) + if err != nil { + return nil, errors.Wrap(err, "selecting players") + } + for i, re := range res { + for _, p := range players { + if re.PlayerSlug == p.Slug { + res[i].Player = p + break + } + } + } + + return res, nil +} diff --git a/db/repository.go b/db/repository.go new file mode 100644 index 0000000..481d58b --- /dev/null +++ b/db/repository.go @@ -0,0 +1,35 @@ +package db + +import ( + "context" + "fmt" + "strings" + + "github.com/uptrace/bun" +) + +type Repository[T any] struct { + db *bun.DB + + pkFieldList []string +} + +func NewRepository[T any](db *bun.DB, pkFieldList []string) *Repository[T] { + return &Repository[T]{db: db, pkFieldList: pkFieldList} +} + +func (r *Repository[T]) CreateOrUpdateMany(ctx context.Context, list []T) error { + if len(list) == 0 { + return nil + } + _, err := r.db.NewInsert().Model(&list). + On(fmt.Sprintf("CONFLICT (%s) DO UPDATE", strings.Join(r.pkFieldList, ","))). + Exec(ctx) + return err +} + +func (r *Repository[T]) GetAll(ctx context.Context) ([]T, error) { + var list []T + err := r.db.NewSelect().Model(&list).Scan(ctx) + return list, err +} diff --git a/db/team.go b/db/team.go new file mode 100644 index 0000000..ba9e0ed --- /dev/null +++ b/db/team.go @@ -0,0 +1,47 @@ +package db + +import ( + "context" + + "github.com/pkg/errors" + "github.com/samber/lo" + "github.com/uptrace/bun" + + "git.lehouerou.net/laurent/sorarebuddy/model" +) + +type TeamRepository struct { + *Repository[model.Team] +} + +func NewTeamRepository(db *bun.DB) *TeamRepository { + return &TeamRepository{ + Repository: NewRepository[model.Team](db, []string{"slug"}), + } +} + +func (r *TeamRepository) GetTeamSlugsNotInDb(ctx context.Context, teamSlugs []string) ([]string, error) { + var teams []model.Team + err := r.db.NewSelect(). + Model(&teams). + Where("slug IN (?)", bun.In(teamSlugs)). + Scan(ctx) + if err != nil { + return nil, errors.Wrap(err, "getting players not in db") + } + diff, _ := lo.Difference(teamSlugs, lo.Map(teams, func(t model.Team, index int) string { return t.Slug })) + return diff, nil +} + +func (r *TeamRepository) SearchByDisplayName(ctx context.Context, displayName string, limit int) ([]model.Team, error) { + var teams []model.Team + err := r.db.NewSelect(). + Model(&teams). + Relation("Country"). + Relation("DomesticLeague"). + Relation("DomesticLeague.Zone"). + Where("f_unaccent(team.display_name) ILIKE ?", "%"+displayName+"%"). + Limit(limit). + Scan(ctx) + return teams, err +} diff --git a/db/zone.go b/db/zone.go new file mode 100644 index 0000000..4ea4a68 --- /dev/null +++ b/db/zone.go @@ -0,0 +1,28 @@ +package db + +import ( + "context" + + "github.com/uptrace/bun" + + "git.lehouerou.net/laurent/sorarebuddy/model" +) + +type ZoneRepository struct { + db *bun.DB +} + +func NewZoneRepository(db *bun.DB) *ZoneRepository { + return &ZoneRepository{db: db} +} + +func (r *ZoneRepository) GetAll(ctx context.Context) ([]model.Zone, error) { + var res []model.Zone + if err := r.db.NewSelect().Model(&res).Scan(ctx); err != nil { + return nil, err + } + + return res, nil +} + +//initializing sorare client: getting current user: querying records: Message: Unauthorized: Signature has expired, Locations: []" diff --git a/front/.eslintrc.cjs b/front/.eslintrc.cjs new file mode 100644 index 0000000..d6c9537 --- /dev/null +++ b/front/.eslintrc.cjs @@ -0,0 +1,18 @@ +module.exports = { + root: true, + env: { browser: true, es2020: true }, + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:react-hooks/recommended', + ], + ignorePatterns: ['dist', '.eslintrc.cjs'], + parser: '@typescript-eslint/parser', + plugins: ['react-refresh'], + rules: { + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, +} diff --git a/front/.gitignore b/front/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/front/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/front/.prettierrc b/front/.prettierrc new file mode 100644 index 0000000..0538fc1 --- /dev/null +++ b/front/.prettierrc @@ -0,0 +1,13 @@ +{ + "trailingComma": "es5", + "tabWidth": 2, + "semi": false, + "singleQuote": false, + "useTabs": false, + "plugins": [ + "@ianvs/prettier-plugin-sort-imports", + "prettier-plugin-tailwindcss" + ], + "pluginSearchDirs": ["."] +} + diff --git a/front/README.md b/front/README.md new file mode 100644 index 0000000..0d6babe --- /dev/null +++ b/front/README.md @@ -0,0 +1,30 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: + +- Configure the top-level `parserOptions` property like this: + +```js +export default { + // other rules... + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + project: ['./tsconfig.json', './tsconfig.node.json'], + tsconfigRootDir: __dirname, + }, +} +``` + +- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` +- Optionally add `plugin:@typescript-eslint/stylistic-type-checked` +- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list diff --git a/front/bun.lockb b/front/bun.lockb new file mode 100644 index 0000000000000000000000000000000000000000..f2eb4d4b98b0d19a8707f56f48380f15452d64eb GIT binary patch literal 161296 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 z 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 + + + + + + Sorare Buddy + + +
+ + + diff --git a/front/package-lock.json b/front/package-lock.json new file mode 100644 index 0000000..5f83db0 --- /dev/null +++ b/front/package-lock.json @@ -0,0 +1,4802 @@ +{ + "name": "sorarebuddy", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "sorarebuddy", + "version": "0.0.0", + "dependencies": { + "@fontsource-variable/exo-2": "^5.0.18", + "@fontsource/roboto": "^5.0.12", + "@tanstack/react-query": "^5.28.9", + "@tanstack/react-query-devtools": "^5.28.10", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router": "^6.22.3", + "react-router-dom": "^6.22.3", + "react-select": "^5.8.0" + }, + "devDependencies": { + "@ianvs/prettier-plugin-sort-imports": "^4.2.1", + "@types/react": "^18.2.66", + "@types/react-dom": "^18.2.22", + "@typescript-eslint/eslint-plugin": "^7.2.0", + "@typescript-eslint/parser": "^7.2.0", + "@vitejs/plugin-react": "^4.2.1", + "autoprefixer": "^10.4.19", + "eslint": "^8.57.0", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.6", + "postcss": "^8.4.38", + "prettier": "^3.2.5", + "prettier-plugin-tailwindcss": "^0.5.13", + "tailwindcss": "^3.4.3", + "typescript": "^5.2.2", + "vite": "^5.2.0" + } + }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "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.24.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz", + "integrity": "sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==", + "dependencies": { + "@babel/highlight": "^7.24.2", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.1.tgz", + "integrity": "sha512-Pc65opHDliVpRHuKfzI+gSA4zcgr65O4cl64fFJIWEEh8JoHIHh0Oez1Eo8Arz8zq/JhgKodQaxEwUPRtZylVA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.24.3", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.3.tgz", + "integrity": "sha512-5FcvN1JHw2sHJChotgx8Ek0lyuh4kCKelgMTTqhYJJtloNvUfpAFMeNQUtdlIaktwrSV9LtCdqwk48wL2wBacQ==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.2", + "@babel/generator": "^7.24.1", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helpers": "^7.24.1", + "@babel/parser": "^7.24.1", + "@babel/template": "^7.24.0", + "@babel/traverse": "^7.24.1", + "@babel/types": "^7.24.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/core/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, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.1.tgz", + "integrity": "sha512-DfCRfZsBcrPEHUfuBMgbJ1Ut01Y/itOs+hY2nFLgqsqXd52/iSiVq5TITtUasIUgm+IIKdY2/1I7auiQOEeC9A==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.0", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", + "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.23.5", + "@babel/helper-validator-option": "^7.23.5", + "browserslist": "^4.22.2", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/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, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.24.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.3.tgz", + "integrity": "sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg==", + "dependencies": { + "@babel/types": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", + "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-validator-identifier": "^7.22.20" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.0.tgz", + "integrity": "sha512-9cUznXMG0+FxRuJfvL82QlTqIzhVW9sL0KjMPHhAOOvpQGL8QtdxnBKILjBqxlHyliz0yCa1G903ZXI/FuHy2w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz", + "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", + "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.1.tgz", + "integrity": "sha512-BpU09QqEe6ZCHuIHFphEFgvNSrubve1FtyMton26ekZ85gRGi6LrTF7zArARp2YvyFxloeiRmtSCq5sjh1WqIg==", + "dev": true, + "dependencies": { + "@babel/template": "^7.24.0", + "@babel/traverse": "^7.24.1", + "@babel/types": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.24.2", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.2.tgz", + "integrity": "sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA==", + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.1.tgz", + "integrity": "sha512-Zo9c7N3xdOIQrNip7Lc9wvRPzlRtovHVE4lkz8WEDr7uYh/GMQhSiIgFxGIArRHYdJE5kxtZjAf8rT0xhdLCzg==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.24.1.tgz", + "integrity": "sha512-kDJgnPujTmAZ/9q2CN4m2/lRsUUPDvsG3+tSHWUJIzMGTt5U/b/fwWd3RO3n+5mjLrsBrVa5eKFRVSQbi3dF1w==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.24.1.tgz", + "integrity": "sha512-1v202n7aUq4uXAieRTKcwPzNyphlCuqHHDcdSNc+vdhoTEZcFMh+L5yZuCmGaIO7bs1nJUNfHB89TZyoL48xNA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.1.tgz", + "integrity": "sha512-+BIznRzyqBf+2wCTxcKE3wDjfGeCoVE61KSHGpkzqrLi8qxqFwBeUFyId2cxkTmm55fzDGnm0+yCxaxygrLUnQ==", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz", + "integrity": "sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.23.5", + "@babel/parser": "^7.24.0", + "@babel/types": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.1.tgz", + "integrity": "sha512-xuU6o9m68KeqZbQuDt2TcKSxUw/mrsvavlEqQ1leZ/B+C9tk6E4sRWy97WaXgvq5E+nU3cXMxv3WKOCanVMCmQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.24.1", + "@babel/generator": "^7.24.1", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.24.1", + "@babel/types": "^7.24.0", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz", + "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==", + "dependencies": { + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emotion/babel-plugin": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz", + "integrity": "sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/serialize": "^1.1.2", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/babel-plugin/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" + }, + "node_modules/@emotion/babel-plugin/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==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@emotion/cache": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz", + "integrity": "sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==", + "dependencies": { + "@emotion/memoize": "^0.8.1", + "@emotion/sheet": "^1.2.2", + "@emotion/utils": "^1.2.1", + "@emotion/weak-memoize": "^0.3.1", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz", + "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==" + }, + "node_modules/@emotion/memoize": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" + }, + "node_modules/@emotion/react": { + "version": "11.11.4", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.11.4.tgz", + "integrity": "sha512-t8AjMlF0gHpvvxk5mAtCqR4vmxiGHCeJBaQO6gncUSdklELOgtwjerNY2yuJNfwnc6vi16U/+uMF+afIawJ9iw==", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.11.0", + "@emotion/cache": "^11.11.0", + "@emotion/serialize": "^1.1.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1", + "@emotion/utils": "^1.2.1", + "@emotion/weak-memoize": "^0.3.1", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.4.tgz", + "integrity": "sha512-RIN04MBT8g+FnDwgvIUi8czvr1LU1alUMI05LekWB5DGyTm8cCBMCRpq3GqaiyEDRptEXOyXnvZ58GZYu4kBxQ==", + "dependencies": { + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/unitless": "^0.8.1", + "@emotion/utils": "^1.2.1", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/sheet": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.2.tgz", + "integrity": "sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==" + }, + "node_modules/@emotion/unitless": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", + "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz", + "integrity": "sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.1.tgz", + "integrity": "sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg==" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz", + "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", + "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", + "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", + "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", + "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", + "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", + "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", + "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", + "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", + "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", + "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", + "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", + "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", + "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", + "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", + "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", + "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", + "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", + "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", + "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", + "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", + "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", + "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", + "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", + "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.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": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/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, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.0.tgz", + "integrity": "sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==", + "dependencies": { + "@floating-ui/utils": "^0.2.1" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.3.tgz", + "integrity": "sha512-RnDthu3mzPlQ31Ss/BTwQ1zjzIhr3lk1gZB1OC56h/1vEtaXkESrOqL5fQVMfXpwGtRwX+YsZBdyHtJMQnkArw==", + "dependencies": { + "@floating-ui/core": "^1.0.0", + "@floating-ui/utils": "^0.2.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz", + "integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==" + }, + "node_modules/@fontsource-variable/exo-2": { + "version": "5.0.18", + "resolved": "https://registry.npmjs.org/@fontsource-variable/exo-2/-/exo-2-5.0.18.tgz", + "integrity": "sha512-3WvBtJHnJeiTcYJ3NMVDWQVP7lV+JEuHyy/bLOUxB9RVN0GpYG8WZT2LYjIbYnKXcBNbcaXoKlvmEY+Pv0WdvA==" + }, + "node_modules/@fontsource/roboto": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-5.0.12.tgz", + "integrity": "sha512-x0o17jvgoSSbS9OZnUX2+xJmVRvVCfeaYJjkS7w62iN7CuJWtMf5vJj8LqgC7ibqIkitOHVW+XssRjgrcHn62g==" + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/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, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "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, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", + "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", + "dev": true + }, + "node_modules/@ianvs/prettier-plugin-sort-imports": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@ianvs/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-4.2.1.tgz", + "integrity": "sha512-NKN1LVFWUDGDGr3vt+6Ey3qPeN/163uR1pOPAlkWpgvAqgxQ6kSdUf1F0it8aHUtKRUzEGcK38Wxd07O61d7+Q==", + "dev": true, + "dependencies": { + "@babel/core": "^7.24.0", + "@babel/generator": "^7.23.6", + "@babel/parser": "^7.24.0", + "@babel/traverse": "^7.24.0", + "@babel/types": "^7.24.0", + "semver": "^7.5.2" + }, + "peerDependencies": { + "@vue/compiler-sfc": "2.7.x || 3.x", + "prettier": "2 || 3" + }, + "peerDependenciesMeta": { + "@vue/compiler-sfc": { + "optional": true + } + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/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, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "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, + "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, + "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, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@remix-run/router": { + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.15.3.tgz", + "integrity": "sha512-Oy8rmScVrVxWZVOpEF57ovlnhpZ8CCPlnIIumVcV9nFdiSIrus99+Lw78ekXyGvVDlIsFJbSfmSovJUhCWYV3w==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.13.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.13.2.tgz", + "integrity": "sha512-3XFIDKWMFZrMnao1mJhnOT1h2g0169Os848NhhmGweEcfJ4rCi+3yMCOLG4zA61rbJdkcrM/DjVZm9Hg5p5w7g==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.13.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.13.2.tgz", + "integrity": "sha512-GdxxXbAuM7Y/YQM9/TwwP+L0omeE/lJAR1J+olu36c3LqqZEBdsIWeQ91KBe6nxwOnb06Xh7JS2U5ooWU5/LgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.13.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.13.2.tgz", + "integrity": "sha512-mCMlpzlBgOTdaFs83I4XRr8wNPveJiJX1RLfv4hggyIVhfB5mJfN4P8Z6yKh+oE4Luz+qq1P3kVdWrCKcMYrrA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.13.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.13.2.tgz", + "integrity": "sha512-yUoEvnH0FBef/NbB1u6d3HNGyruAKnN74LrPAfDQL3O32e3k3OSfLrPgSJmgb3PJrBZWfPyt6m4ZhAFa2nZp2A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.13.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.13.2.tgz", + "integrity": "sha512-GYbLs5ErswU/Xs7aGXqzc3RrdEjKdmoCrgzhJWyFL0r5fL3qd1NPcDKDowDnmcoSiGJeU68/Vy+OMUluRxPiLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.13.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.13.2.tgz", + "integrity": "sha512-L1+D8/wqGnKQIlh4Zre9i4R4b4noxzH5DDciyahX4oOz62CphY7WDWqJoQ66zNR4oScLNOqQJfNSIAe/6TPUmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.13.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.13.2.tgz", + "integrity": "sha512-tK5eoKFkXdz6vjfkSTCupUzCo40xueTOiOO6PeEIadlNBkadH1wNOH8ILCPIl8by/Gmb5AGAeQOFeLev7iZDOA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.13.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.13.2.tgz", + "integrity": "sha512-zvXvAUGGEYi6tYhcDmb9wlOckVbuD+7z3mzInCSTACJ4DQrdSLPNUeDIcAQW39M3q6PDquqLWu7pnO39uSMRzQ==", + "cpu": [ + "ppc64le" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.13.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.13.2.tgz", + "integrity": "sha512-C3GSKvMtdudHCN5HdmAMSRYR2kkhgdOfye4w0xzyii7lebVr4riCgmM6lRiSCnJn2w1Xz7ZZzHKuLrjx5620kw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.13.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.13.2.tgz", + "integrity": "sha512-l4U0KDFwzD36j7HdfJ5/TveEQ1fUTjFFQP5qIt9gBqBgu1G8/kCaq5Ok05kd5TG9F8Lltf3MoYsUMw3rNlJ0Yg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.13.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.13.2.tgz", + "integrity": "sha512-xXMLUAMzrtsvh3cZ448vbXqlUa7ZL8z0MwHp63K2IIID2+DeP5iWIT6g1SN7hg1VxPzqx0xZdiDM9l4n9LRU1A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.13.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.13.2.tgz", + "integrity": "sha512-M/JYAWickafUijWPai4ehrjzVPKRCyDb1SLuO+ZyPfoXgeCEAlgPkNXewFZx0zcnoIe3ay4UjXIMdXQXOZXWqA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.13.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.13.2.tgz", + "integrity": "sha512-2YWwoVg9KRkIKaXSh0mz3NmfurpmYoBBTAXA9qt7VXk0Xy12PoOP40EFuau+ajgALbbhi4uTj3tSG3tVseCjuA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.13.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.13.2.tgz", + "integrity": "sha512-2FSsE9aQ6OWD20E498NYKEQLneShWes0NGMPQwxWOdws35qQXH+FplabOSP5zEe1pVjurSDOGEVCE2agFwSEsw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.13.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.13.2.tgz", + "integrity": "sha512-7h7J2nokcdPePdKykd8wtc8QqqkqxIrUz7MHj6aNr8waBRU//NLDVnNjQnqQO6fqtjrtCdftpbTuOKAyrAQETQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tanstack/query-core": { + "version": "5.28.9", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.28.9.tgz", + "integrity": "sha512-hNlfCiqZevr3GRVPXS3MhaGW5hjcxvCsIQ4q6ff7EPlvFwYZaS+0d9EIIgofnegDaU2BbCDlyURoYfRl5rmzow==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/query-devtools": { + "version": "5.28.10", + "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.28.10.tgz", + "integrity": "sha512-5UN629fKa5/1K/2Pd26gaU7epxRrYiT1gy+V+pW5K6hnf1DeUKK3pANSb2eHKlecjIKIhTwyF7k9XdyE2gREvQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.28.9", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.28.9.tgz", + "integrity": "sha512-vwifBkGXsydsLxFOBMe3+f8kvtDoqDRDwUNjPHVDDt+FoBetCbOWAUHgZn4k+CVeZgLmy7bx6aKeDbe3e8koOQ==", + "dependencies": { + "@tanstack/query-core": "5.28.9" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18.0.0" + } + }, + "node_modules/@tanstack/react-query-devtools": { + "version": "5.28.10", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.28.10.tgz", + "integrity": "sha512-D+SiHZTWhK2sNgBYj+xIvUOqonsKy74OLU/YHmRB5OZVLLTiekvZd12C3rKlU+WM69jid0hjEjuFqkULOMwc3A==", + "dependencies": { + "@tanstack/query-devtools": "5.28.10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-query": "^5.28.9", + "react": "^18.0.0" + } + }, + "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, + "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.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dev": true, + "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, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.5.tgz", + "integrity": "sha512-WXCyOcRtH37HAUkpXhUduaxdm82b4GSlyTqajXviN4EfiuPgNYR109xMCKvpl6zPIpua0DGlMEDCq+g8EdoheQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true + }, + "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 + }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==" + }, + "node_modules/@types/prop-types": { + "version": "15.7.12", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", + "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==" + }, + "node_modules/@types/react": { + "version": "18.2.73", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.73.tgz", + "integrity": "sha512-XcGdod0Jjv84HOC7N5ziY3x+qL0AfmubvKOZ9hJjJ2yd5EE+KYjWhdOjt387e9HPheHkdggF9atTifMRtyAaRA==", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.2.23", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.23.tgz", + "integrity": "sha512-ZQ71wgGOTmDYpnav2knkjr3qXdAFu0vsk8Ci5w3pGAIdj7/kKAyn+VsQDhXsmzzzepAiI9leWMmubXz690AI/A==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/react-transition-group": { + "version": "4.4.10", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz", + "integrity": "sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/semver": { + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", + "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", + "dev": true + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.4.0.tgz", + "integrity": "sha512-yHMQ/oFaM7HZdVrVm/M2WHaNPgyuJH4WelkSVEWSSsir34kxW2kDJCxlXRhhGWEsMN0WAW/vLpKfKVcm8k+MPw==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "7.4.0", + "@typescript-eslint/type-utils": "7.4.0", + "@typescript-eslint/utils": "7.4.0", + "@typescript-eslint/visitor-keys": "7.4.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.4.0.tgz", + "integrity": "sha512-ZvKHxHLusweEUVwrGRXXUVzFgnWhigo4JurEj0dGF1tbcGh6buL+ejDdjxOQxv6ytcY1uhun1p2sm8iWStlgLQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "7.4.0", + "@typescript-eslint/types": "7.4.0", + "@typescript-eslint/typescript-estree": "7.4.0", + "@typescript-eslint/visitor-keys": "7.4.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.4.0.tgz", + "integrity": "sha512-68VqENG5HK27ypafqLVs8qO+RkNc7TezCduYrx8YJpXq2QGZ30vmNZGJJJC48+MVn4G2dCV8m5ZTVnzRexTVtw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.4.0", + "@typescript-eslint/visitor-keys": "7.4.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.4.0.tgz", + "integrity": "sha512-247ETeHgr9WTRMqHbbQdzwzhuyaJ8dPTuyuUEMANqzMRB1rj/9qFIuIXK7l0FX9i9FXbHeBQl/4uz6mYuCE7Aw==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "7.4.0", + "@typescript-eslint/utils": "7.4.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.4.0.tgz", + "integrity": "sha512-mjQopsbffzJskos5B4HmbsadSJQWaRK0UxqQ7GuNA9Ga4bEKeiO6b2DnB6cM6bpc8lemaPseh0H9B/wyg+J7rw==", + "dev": true, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.4.0.tgz", + "integrity": "sha512-A99j5AYoME/UBQ1ucEbbMEmGkN7SE0BvZFreSnTd1luq7yulcHdyGamZKizU7canpGDWGJ+Q6ZA9SyQobipePg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.4.0", + "@typescript-eslint/visitor-keys": "7.4.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.4.0.tgz", + "integrity": "sha512-NQt9QLM4Tt8qrlBVY9lkMYzfYtNz8/6qwZg8pI3cMGlPnj6mOpRxxAm7BMJN9K0AiY+1BwJ5lVC650YJqYOuNg==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "7.4.0", + "@typescript-eslint/types": "7.4.0", + "@typescript-eslint/typescript-estree": "7.4.0", + "semver": "^7.5.4" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.4.0.tgz", + "integrity": "sha512-0zkC7YM0iX5Y41homUUeW1CHtZR01K3ybjM1l6QczoMuay0XKtrb93kv95AxUGwdjGr64nNqnOCwmEl616N8CA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.4.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.2.1.tgz", + "integrity": "sha512-oojO9IDc4nCUUi8qIR11KoQm0XFFLIwsRBwHRR4d/88IWghn1y6ckz/bJ8GHDCsYEJee8mDzqtJxh15/cisJNQ==", + "dev": true, + "dependencies": { + "@babel/core": "^7.23.5", + "@babel/plugin-transform-react-jsx-self": "^7.23.3", + "@babel/plugin-transform-react-jsx-source": "^7.23.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.14.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0" + } + }, + "node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "dev": true, + "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, + "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, + "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-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true + }, + "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 + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.19", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz", + "integrity": "sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "browserslist": "^4.23.0", + "caniuse-lite": "^1.0.30001599", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, + "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 + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", + "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", + "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" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001587", + "electron-to-chromium": "^1.4.668", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.13" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001600", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001600.tgz", + "integrity": "sha512-+2S9/2JFhYmYaDpZvo0lKkfvuKIglrx68MwOBqMGHhQsNkLjB5xtc/TGoEPs+MxjSyN/72qer2g97nzR641mOQ==", + "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" + } + ] + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/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, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "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 + }, + "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 + }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cosmiconfig/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "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 + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "node_modules/electron-to-chromium": { + "version": "1.4.721", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.721.tgz", + "integrity": "sha512-k1x2r6foI8iJOp+1qTxbbrrWMsOiHkzGBYwYigaq+apO1FSqtn44KTo3Sy69qt7CRr7149zTcsDvH7MUKsOuIQ==", + "dev": true + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/esbuild": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", + "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.20.2", + "@esbuild/android-arm": "0.20.2", + "@esbuild/android-arm64": "0.20.2", + "@esbuild/android-x64": "0.20.2", + "@esbuild/darwin-arm64": "0.20.2", + "@esbuild/darwin-x64": "0.20.2", + "@esbuild/freebsd-arm64": "0.20.2", + "@esbuild/freebsd-x64": "0.20.2", + "@esbuild/linux-arm": "0.20.2", + "@esbuild/linux-arm64": "0.20.2", + "@esbuild/linux-ia32": "0.20.2", + "@esbuild/linux-loong64": "0.20.2", + "@esbuild/linux-mips64el": "0.20.2", + "@esbuild/linux-ppc64": "0.20.2", + "@esbuild/linux-riscv64": "0.20.2", + "@esbuild/linux-s390x": "0.20.2", + "@esbuild/linux-x64": "0.20.2", + "@esbuild/netbsd-x64": "0.20.2", + "@esbuild/openbsd-x64": "0.20.2", + "@esbuild/sunos-x64": "0.20.2", + "@esbuild/win32-arm64": "0.20.2", + "@esbuild/win32-ia32": "0.20.2", + "@esbuild/win32-x64": "0.20.2" + } + }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eslint": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", + "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", + "dev": true, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.6.tgz", + "integrity": "sha512-NjGXdm7zgcKRkKMua34qVO9doI7VOxZ6ancSvBELJSSoX97jyndXcSoa8XBh69JoB31dNz3EEzlMcizZl7LaMA==", + "dev": true, + "peerDependencies": { + "eslint": ">=7" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "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, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/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, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/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, + "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/eslint/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, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/eslint/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 + }, + "node_modules/eslint/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, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/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, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/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, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint/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, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "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, + "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, + "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, + "engines": { + "node": ">=0.10.0" + } + }, + "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 + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "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.4" + }, + "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, + "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 + }, + "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 + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" + }, + "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, + "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": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true + }, + "node_modules/foreground-child": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", + "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "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==", + "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, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/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, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/ignore": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "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, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "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, + "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, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/jackspeak": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", + "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jiti": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz", + "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==", + "dev": true, + "bin": { + "jiti": "bin/jiti.js" + } + }, + "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==" + }, + "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, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "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 + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + }, + "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 + }, + "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 + }, + "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, + "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, + "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, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" + }, + "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, + "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 + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "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, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==" + }, + "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, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "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 + }, + "node_modules/node-releases": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + }, + "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, + "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, + "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==", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "node_modules/path-scurry": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.2.tgz", + "integrity": "sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==", + "dev": true, + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", + "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", + "dev": true, + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.4.38", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", + "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "dev": true, + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-load-config/node_modules/lilconfig": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.1.tgz", + "integrity": "sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/postcss-nested": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", + "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.11" + }, + "engines": { + "node": ">=12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.16", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.16.tgz", + "integrity": "sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "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, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", + "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-plugin-organize-imports": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-3.2.4.tgz", + "integrity": "sha512-6m8WBhIp0dfwu0SkgfOxJqh+HpdyfqSSLfKKRZSFbDuEQXDDndb8fTpRWkUrX/uBenkex3MgnVk0J3b3Y5byog==", + "dev": true, + "optional": true, + "peer": true, + "peerDependencies": { + "@volar/vue-language-plugin-pug": "^1.0.4", + "@volar/vue-typescript": "^1.0.4", + "prettier": ">=2.0", + "typescript": ">=2.9" + }, + "peerDependenciesMeta": { + "@volar/vue-language-plugin-pug": { + "optional": true + }, + "@volar/vue-typescript": { + "optional": true + } + } + }, + "node_modules/prettier-plugin-tailwindcss": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.5.13.tgz", + "integrity": "sha512-2tPWHCFNC+WRjAC4SIWQNSOdcL1NNkydXim8w7TDqlZi+/ulZYz2OouAI6qMtkggnPt7lGamboj6LcTMwcCvoQ==", + "dev": true, + "engines": { + "node": ">=14.21.3" + }, + "peerDependencies": { + "@ianvs/prettier-plugin-sort-imports": "*", + "@prettier/plugin-pug": "*", + "@shopify/prettier-plugin-liquid": "*", + "@trivago/prettier-plugin-sort-imports": "*", + "@zackad/prettier-plugin-twig-melody": "*", + "prettier": "^3.0", + "prettier-plugin-astro": "*", + "prettier-plugin-css-order": "*", + "prettier-plugin-import-sort": "*", + "prettier-plugin-jsdoc": "*", + "prettier-plugin-marko": "*", + "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-pug": { + "optional": true + }, + "@shopify/prettier-plugin-liquid": { + "optional": true + }, + "@trivago/prettier-plugin-sort-imports": { + "optional": true + }, + "@zackad/prettier-plugin-twig-melody": { + "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-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/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "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, + "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" + } + ] + }, + "node_modules/react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" + }, + "peerDependencies": { + "react": "^18.2.0" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "node_modules/react-refresh": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", + "integrity": "sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "6.22.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.22.3.tgz", + "integrity": "sha512-dr2eb3Mj5zK2YISHK++foM9w4eBnO23eKnZEDs7c880P6oKbrjz/Svg9+nxqtHQK+oMW4OtjZca0RqPglXxguQ==", + "dependencies": { + "@remix-run/router": "1.15.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.22.3", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.22.3.tgz", + "integrity": "sha512-7ZILI7HjcE+p31oQvwbokjk6OA/bnFxrhJ19n82Ex9Ph8fNAq+Hm/7KchpMGlTgWhUxRHMMCut+vEtNpWpowKw==", + "dependencies": { + "@remix-run/router": "1.15.3", + "react-router": "6.22.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/react-select": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/react-select/-/react-select-5.8.0.tgz", + "integrity": "sha512-TfjLDo58XrhP6VG5M/Mi56Us0Yt8X7xD6cDybC7yoRMUNm7BGO7qk8J0TLQOua/prb8vUOtsfnXZwfm30HGsAA==", + "dependencies": { + "@babel/runtime": "^7.12.0", + "@emotion/cache": "^11.4.0", + "@emotion/react": "^11.8.1", + "@floating-ui/dom": "^1.0.1", + "@types/react-transition-group": "^4.4.0", + "memoize-one": "^6.0.0", + "prop-types": "^15.6.0", + "react-transition-group": "^4.3.0", + "use-isomorphic-layout-effect": "^1.1.2" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "4.13.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.13.2.tgz", + "integrity": "sha512-MIlLgsdMprDBXC+4hsPgzWUasLO9CE4zOkj/u6j+Z6j5A4zRY+CtiXAdJyPtgCsc42g658Aeh1DlrdVEJhsL2g==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.5" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.13.2", + "@rollup/rollup-android-arm64": "4.13.2", + "@rollup/rollup-darwin-arm64": "4.13.2", + "@rollup/rollup-darwin-x64": "4.13.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.13.2", + "@rollup/rollup-linux-arm64-gnu": "4.13.2", + "@rollup/rollup-linux-arm64-musl": "4.13.2", + "@rollup/rollup-linux-powerpc64le-gnu": "4.13.2", + "@rollup/rollup-linux-riscv64-gnu": "4.13.2", + "@rollup/rollup-linux-s390x-gnu": "4.13.2", + "@rollup/rollup-linux-x64-gnu": "4.13.2", + "@rollup/rollup-linux-x64-musl": "4.13.2", + "@rollup/rollup-win32-arm64-msvc": "4.13.2", + "@rollup/rollup-win32-ia32-msvc": "4.13.2", + "@rollup/rollup-win32-x64-msvc": "4.13.2", + "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" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "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, + "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, + "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, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/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, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "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, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" + }, + "node_modules/sucrase": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/sucrase/node_modules/glob": { + "version": "10.3.12", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz", + "integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.6", + "minimatch": "^9.0.1", + "minipass": "^7.0.4", + "path-scurry": "^1.10.2" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.3.tgz", + "integrity": "sha512-U7sxQk/n397Bmx4JHbJx/iSOOv5G+II3f1kpLpY2QeUv5DcPdcTsYLlusZfq1NthHS1c1cZoyFmmkex1rzke0A==", + "dev": true, + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.5.3", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.0", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.0", + "lilconfig": "^2.1.0", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.23", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.1", + "postcss-nested": "^6.0.1", + "postcss-selector-parser": "^6.0.11", + "resolve": "^1.22.2", + "sucrase": "^3.32.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "engines": { + "node": ">=4" + } + }, + "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, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true + }, + "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, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.3.tgz", + "integrity": "sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "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" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "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, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-isomorphic-layout-effect": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz", + "integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/vite": { + "version": "5.2.7", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.7.tgz", + "integrity": "sha512-k14PWOKLI6pMaSzAuGtT+Cf0YmIx12z9YGon39onaJNy8DLBfBJrzg9FQEmkAM5lpHBZs9wksWAsyF/HkpEwJA==", + "dev": true, + "dependencies": { + "esbuild": "^0.20.1", + "postcss": "^8.4.38", + "rollup": "^4.13.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "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, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/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, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/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, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi-cjs/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 + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?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, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/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, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/yaml": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.1.tgz", + "integrity": "sha512-pIXzoImaqmfOrL7teGUBt/T7ZDnyeGBWyXQBvOVhLkWLN37GXv8NMLK406UY6dS51JfcQHsmcW5cJ441bHg6Lg==", + "dev": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, + "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, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/front/package.json b/front/package.json new file mode 100644 index 0000000..71df1be --- /dev/null +++ b/front/package.json @@ -0,0 +1,46 @@ +{ + "name": "sorarebuddy", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "export": "tsc && vite build --outDir ../cmd/server/dist --emptyOutDir", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview" + }, + "dependencies": { + "@fontsource-variable/exo-2": "^5.0.18", + "@fontsource/roboto": "^5.0.12", + "@tanstack/react-query": "^5.28.9", + "@tanstack/react-query-devtools": "^5.28.10", + "date-fns": "^3.6.0", + "moment": "^2.30.1", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-icons": "^5.1.0", + "react-router": "^6.22.3", + "react-router-dom": "^6.22.3", + "react-select": "^5.8.0", + "recharts": "^2.12.5" + }, + "devDependencies": { + "@ianvs/prettier-plugin-sort-imports": "^4.2.1", + "@types/react": "^18.2.66", + "@types/react-dom": "^18.2.22", + "@typescript-eslint/eslint-plugin": "^7.2.0", + "@typescript-eslint/parser": "^7.2.0", + "@vitejs/plugin-react": "^4.2.1", + "autoprefixer": "^10.4.19", + "eslint": "^8.57.0", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.6", + "postcss": "^8.4.38", + "prettier": "^3.2.5", + "prettier-plugin-tailwindcss": "^0.5.13", + "tailwindcss": "^3.4.3", + "typescript": "^5.2.2", + "vite": "^5.2.0" + } +} diff --git a/front/postcss.config.js b/front/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/front/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/front/public/favicon.ico b/front/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..9414790692fa35648bb6d26a48937b5f10d7a1aa GIT binary patch literal 162228 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

5c>K4^j^f>iOzh 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¨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 z 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 z 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 literal 0 HcmV?d00001 diff --git a/front/src/App.tsx b/front/src/App.tsx new file mode 100644 index 0000000..765ff64 --- /dev/null +++ b/front/src/App.tsx @@ -0,0 +1,18 @@ +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import RouterOutlet from "./router/router"; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + refetchOnWindowFocus: true, + }, + }, +}); + +export default function App() { + return ( + + + + ); +} diff --git a/front/src/api/api.ts b/front/src/api/api.ts new file mode 100644 index 0000000..7eb2511 --- /dev/null +++ b/front/src/api/api.ts @@ -0,0 +1,86 @@ +import Player from "../types/player" +import Team from "../types/team" + +interface GetParams { + endpoint: string + params?: Record +} + +interface PostParams { + endpoint: string + body: unknown +} + +export function Get({ endpoint, params }: GetParams): Promise { + return apiRequest({ method: "GET", endpoint, params }) +} + +export function Post({ endpoint, body }: PostParams): Promise { + return apiRequest({ method: "POST", endpoint, body }) +} + +interface ApiRequestParams { + method: "GET" | "POST" + endpoint: string + body?: unknown + params?: Record +} + +async function apiRequest({ + method, + endpoint, + body, + params, +}: ApiRequestParams): Promise { + const options: RequestInit = { + method, + } + + if (method === "POST") { + options.headers = { "Content-Type": "application/json" } + if (body) { + options.body = JSON.stringify(body) + } + } + + const queryParams = params + ? "?" + + Object.entries(params) + .map( + ([key, value]) => + encodeURIComponent(key) + "=" + encodeURIComponent(value) + ) + .join("&") + : "" + + let resp: Response + try { + resp = await fetch(endpoint + queryParams, options) + } catch (error: unknown) { + if (error instanceof Error) { + throw new Error("Network error: " + error.message) + } else { + throw new Error("An unknown error occurred") + } + } + if (!resp.ok) { + const data = await resp.json() + throw new Error(data.error || "Something went wrong") + } + if (resp.status === 204) return null as unknown as T + return await resp.json() +} + +interface SearchMultiResponse { + players: Player[] + teams: Team[] +} +export async function SearchMulti(query: string): Promise { + return await Post({ + endpoint: "/api/search/multi", + body: { + query: query, + limit: 10, + }, + }) +} diff --git a/front/src/api/card.ts b/front/src/api/card.ts new file mode 100644 index 0000000..23a89fa --- /dev/null +++ b/front/src/api/card.ts @@ -0,0 +1,9 @@ +import Card from "../types/card" +import { Get } from "./api" + +export async function GetUserCards(userSlug: string): Promise { + const raw = await Get({ + endpoint: `/api/user/${userSlug}/cards`, + }) + return raw +} diff --git a/front/src/api/competition.ts b/front/src/api/competition.ts new file mode 100644 index 0000000..74844f0 --- /dev/null +++ b/front/src/api/competition.ts @@ -0,0 +1,11 @@ +import Competition from "../types/competition"; +import { Get } from "./api"; + +export async function GetClubCompetitions( + zones: number[], +): Promise { + return await Get({ + endpoint: `/api/competition/club`, + params: { zones: zones.join(",") }, + }); +} \ No newline at end of file diff --git a/front/src/api/fixture.ts b/front/src/api/fixture.ts new file mode 100644 index 0000000..47c9657 --- /dev/null +++ b/front/src/api/fixture.ts @@ -0,0 +1,9 @@ +import { Fixture } from "../types/fixture" +import { Get } from "./api" + +export async function GetAvailableFixtures(): Promise { + const raw = await Get({ + endpoint: "/api/fixture/all", + }) + return raw.sort((a, b) => b.gameWeek - a.gameWeek) +} diff --git a/front/src/api/game.ts b/front/src/api/game.ts new file mode 100644 index 0000000..0783983 --- /dev/null +++ b/front/src/api/game.ts @@ -0,0 +1,16 @@ +import { Game } from "../types/game" +import { Get } from "./api" + +export async function GetFixtureGames(fixtureSlug: string): Promise { + const raw = await Get({ + endpoint: `/api/fixture/${fixtureSlug}/games`, + }) + return raw + .sort( + (a: Game, b: Game) => + new Date(a.date).getTime() - new Date(b.date).getTime() + ) + .filter((game: Game) => { + return game.awayTeam.slug !== "" && game.homeTeam.slug !== "" + }) +} diff --git a/front/src/api/player.ts b/front/src/api/player.ts new file mode 100644 index 0000000..d225123 --- /dev/null +++ b/front/src/api/player.ts @@ -0,0 +1,17 @@ +import { GamePlayer } from "../types/game" +import Player from "../types/player" +import { Get } from "./api" + +export async function GetPlayer(slug: string): Promise { + const raw = await Get({ + endpoint: `/api/player/${slug}`, + }) + return raw +} + +export async function GetPlayerGames(slug: string): Promise { + const raw = await Get({ + endpoint: `/api/player/${slug}/games`, + }) + return raw +} diff --git a/front/src/api/rankings.ts b/front/src/api/rankings.ts new file mode 100644 index 0000000..9e5cbf5 --- /dev/null +++ b/front/src/api/rankings.ts @@ -0,0 +1,74 @@ +import Player from "../types/player" +import { Post } from "./api" + +export interface SingleRankingsParams { + startDate: Date + position: string + competitions: string[] + zones: number[] + onlyClubGames: boolean + onlyStarting: boolean + minGameCount: number + minTeamGamesPlayedPercentage: number + order: string + limit: number + rarity: string + u23: boolean + minTotalMinutes: number + minAge: number + maxAge: number + hasGameInNextGw: boolean +} + +export interface SingleRanking { + playerSlug: string + teamSlug: string + avgTeamGoalsFor: number + avgTeamGoalsAgainst: number + totalAvgScore: number + decisiveAvgScore: number + allAroundAvgScore: number + minScore: number + maxScore: number + totalStddevScore: number + gameUsedInStatsCount: number + gameStartedCount: number + gameBenchedPlayedCount: number + gameBenchedUnplayedCount: number + teamsGameCount: number + totalPossibleMinutes: number + percentageMinutesPlayed: number + stackPlayPercentage: number + floor: number + moreThan80Score: number + moreThan70Score: number + moreThan60Score: number + moreThan50Score: number + decisiveCount: number + moreThan40AA: number + moreThan30AA: number + moreThan20AA: number + moreThan10AA: number + moreThan80ScorePercentage: number + moreThan70ScorePercentage: number + moreThan60ScorePercentage: number + moreThan50ScorePercentage: number + decisiveCountPercentage: number + moreThan40AAPercentage: number + moreThan30AAPercentage: number + moreThan20AAPercentage: number + moreThan10AAPercentage: number + aaPerMin: number + totalMinutes: number + scores: number[] + player: Player +} + +export async function GetSingleRankings( + options: SingleRankingsParams +): Promise { + return await Post({ + endpoint: "/api/rankings/single", + body: options, + }) +} diff --git a/front/src/api/zone.ts b/front/src/api/zone.ts new file mode 100644 index 0000000..2397b53 --- /dev/null +++ b/front/src/api/zone.ts @@ -0,0 +1,9 @@ +import Zone from "../types/zone"; +import { Get } from "./api"; + +export async function GetAllZones(): Promise { + return await Get({ + endpoint: "/api/zone/all" + }); +} + diff --git a/front/src/components/checkbox.tsx b/front/src/components/checkbox.tsx new file mode 100644 index 0000000..ec86bdf --- /dev/null +++ b/front/src/components/checkbox.tsx @@ -0,0 +1,51 @@ +import { ChangeEventHandler } from "react" + +interface CheckboxProps { + checked: boolean + label: string + onChange: ChangeEventHandler + id: string +} + +export default function Checkbox({ + checked, + label, + onChange, + id, +}: CheckboxProps) { + return ( +

+ ) +} diff --git a/front/src/components/error.tsx b/front/src/components/error.tsx new file mode 100644 index 0000000..9a497f3 --- /dev/null +++ b/front/src/components/error.tsx @@ -0,0 +1,13 @@ +export default function ErrorBlock({ error }: { error: Error }) { + return ( +
+ Error: {error.message} + +
+ ) +} diff --git a/front/src/components/input_wrapper.tsx b/front/src/components/input_wrapper.tsx new file mode 100644 index 0000000..e9f3306 --- /dev/null +++ b/front/src/components/input_wrapper.tsx @@ -0,0 +1,29 @@ +import React from "react"; + +interface InputWrapperProps { + label: string; + id: string; + children: React.ReactNode; + className?: string; +} + +function InputWrapper({ + label, + id, + children, + className = "", +}: InputWrapperProps) { + return ( +
+ + {children} +
+ ); +} + +export default InputWrapper; diff --git a/front/src/components/loader.tsx b/front/src/components/loader.tsx new file mode 100644 index 0000000..e830f71 --- /dev/null +++ b/front/src/components/loader.tsx @@ -0,0 +1,25 @@ +export default function Loader() { + return ( +
+
+ + Loading... +
+
+ ) +} diff --git a/front/src/components/numeric_input.tsx b/front/src/components/numeric_input.tsx new file mode 100644 index 0000000..c6ce32b --- /dev/null +++ b/front/src/components/numeric_input.tsx @@ -0,0 +1,33 @@ +import InputWrapper from "./input_wrapper"; + +interface NumericInputProps { + value: number; + onChange: (value: number) => void; + label: string; + placeholder?: string; + id: string; + classname?: string; +} +export default function NumericInput({ + value, + onChange, + label, + placeholder = label, + id, + classname = "", +}: NumericInputProps) { + return ( + + { + onChange(parseInt(e.target.value)); + }} + /> + + ); +} diff --git a/front/src/components/searchBox.tsx b/front/src/components/searchBox.tsx new file mode 100644 index 0000000..7f82cae --- /dev/null +++ b/front/src/components/searchBox.tsx @@ -0,0 +1,123 @@ +import { useCallback, useState } from "react" +import { useNavigate } from "react-router-dom" +import AsyncSelect from "react-select/async" +import { SearchMulti } from "../api/api" +import Player from "../types/player" +import Team from "../types/team" + +type SearchOption = { + value: string + label: string + imageUrl: string + type: "player" | "team" + data: Player | Team +} + +type GroupedOptionType = { + label: string + options: SearchOption[] +} + +export function SearchBox() { + const [selectedOption, setSelectedOption] = useState( + null + ) + const [searchTimeout, setSearchTimeout] = useState(null) + const navigate = useNavigate() + + const handleSearch = useCallback( + (inputValue: string, callback: (res: GroupedOptionType[]) => void) => { + if (searchTimeout) { + clearTimeout(searchTimeout) + } + setSearchTimeout( + setTimeout(() => { + SearchMulti(inputValue).then((res) => { + const playerOptions = res.players + ? res.players.map((player) => ({ + value: player.slug, + label: + player.displayName + " (" + player.team?.displayName + ")", + type: "player" as const, + data: player, + imageUrl: player.avatarUrl, + })) + : [] + + const teamOptions = res.teams + ? res.teams.map((team) => ({ + value: team.slug, + label: team.displayName, + type: "team" as const, + data: team, + imageUrl: team.pictureUrl, + })) + : [] + + callback([ + { label: "Players", options: playerOptions }, + { label: "Teams", options: teamOptions }, + ]) + }) + }, 200) + ) + }, + [searchTimeout] + ) + + const handleSelect = useCallback( + async (option: SearchOption | null) => { + if (option) { + setSelectedOption(null) + navigate("/" + option.type + "/" + option.value) + } + }, + [navigate] + ) + + const formatGroupLabel = (data: GroupedOptionType) => ( +
+ {data.label} +
+ ) + + const formatOptionLabel = ({ imageUrl, type, data }: SearchOption) => { + return ( +
+
+
+ +
+ {data.displayName} +
+ {type === "player" ? ( +
+ + {(data as Player).team?.displayName} + + +
+ ) : null} +
+ ) + } + + return ( + + ) +} diff --git a/front/src/components/withdatafetching.tsx b/front/src/components/withdatafetching.tsx new file mode 100644 index 0000000..15bdbfd --- /dev/null +++ b/front/src/components/withdatafetching.tsx @@ -0,0 +1,35 @@ +import { useQuery } from "@tanstack/react-query" +import ErrorBlock from "./error" +import Loader from "./loader" + +interface WithDataFetchingProps { + queryKey: string[] + queryFn: () => Promise + refetchInterval?: number + refetchOnWindowFocus?: boolean + enabled?: boolean + children: (data: T) => React.ReactNode +} + +export default function WithDataFetching({ + queryKey, + queryFn, + refetchInterval, + refetchOnWindowFocus, + enabled, + children, +}: WithDataFetchingProps) { + const { data, isLoading, isError, error } = useQuery({ + queryKey: queryKey, + queryFn: queryFn, + refetchInterval: refetchInterval, + refetchOnWindowFocus: refetchOnWindowFocus, + enabled: enabled, + }) + + if (isLoading) return + if (isError) return + if (!data) return ErrorBlock({ error: new Error("Data not found") }) + + return <>{children(data)} +} diff --git a/front/src/global.css b/front/src/global.css new file mode 100644 index 0000000..64959ae --- /dev/null +++ b/front/src/global.css @@ -0,0 +1,14 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + + +@layer components { + .btn { + @apply px-4 py-2 rounded-md text-neutral-50 text-sm font-semibold hover:shadow-md; + } +} + +body { + @apply font-sans; +} \ No newline at end of file diff --git a/front/src/main.tsx b/front/src/main.tsx new file mode 100644 index 0000000..ff2446a --- /dev/null +++ b/front/src/main.tsx @@ -0,0 +1,12 @@ +import "@fontsource/roboto/latin.css" +import "@fontsource-variable/exo-2" +import React from "react" +import ReactDOM from "react-dom/client" +import App from "./App.tsx" +import "./global.css" + +ReactDOM.createRoot(document.getElementById("root")!).render( + + + +) diff --git a/front/src/pages/header.tsx b/front/src/pages/header.tsx new file mode 100644 index 0000000..198512d --- /dev/null +++ b/front/src/pages/header.tsx @@ -0,0 +1,35 @@ +import { Link, useLocation } from "react-router-dom" +import { SearchBox } from "../components/searchBox" + +interface HeaderLinkButtonProps { + to: string + label: string +} + +const HeaderLinkButton = ({ to, label }: HeaderLinkButtonProps) => { + const location = useLocation() + const isActive = location.pathname === to + return ( + + {label} + + ) +} + +export default function Header() { + return ( +
+
+ +
+
+ + + +
+
+ ) +} diff --git a/front/src/pages/layout.tsx b/front/src/pages/layout.tsx new file mode 100644 index 0000000..9952b08 --- /dev/null +++ b/front/src/pages/layout.tsx @@ -0,0 +1,13 @@ +import { Outlet } from "react-router-dom" +import Header from "./header" + +export default function Layout() { + return ( + <> +
+
+ +
+ + ) +} diff --git a/front/src/pages/live/index.tsx b/front/src/pages/live/index.tsx new file mode 100644 index 0000000..58f43e5 --- /dev/null +++ b/front/src/pages/live/index.tsx @@ -0,0 +1,374 @@ +import { useEffect, useMemo, useState } from "react" +import { GetAvailableFixtures } from "../../api/fixture" +import { GetFixtureGames } from "../../api/game" +import WithDataFetching from "../../components/withdatafetching" +import { Fixture } from "../../types/fixture" +import { Game } from "../../types/game" + +export default function Live() { + return ( + + queryKey={["availableFixtures"]} + queryFn={GetAvailableFixtures} + refetchInterval={60 * 60000} + > + {(data) => } + + ) +} + +function FixtureListLayout({ fixtures }: { fixtures: Fixture[] }) { + const [currentIndex, setCurrentIndex] = useState(() => { + const startedFixtureIndex = fixtures?.findIndex( + (fixture) => fixture.fixtureState === "started" + ) + return startedFixtureIndex !== -1 ? startedFixtureIndex : 0 + }) + + useEffect(() => { + if (currentIndex < 0) { + setCurrentIndex(0) + } else if (currentIndex >= fixtures.length) { + setCurrentIndex(fixtures.length - 1) + } + }, [currentIndex, fixtures?.length]) + + const selectedFixture = fixtures[currentIndex] + + const handlePrevious = () => { + if (currentIndex > 0) { + setCurrentIndex(currentIndex - 1) + } + } + + const handleNext = () => { + if (currentIndex < fixtures.length - 1) { + setCurrentIndex(currentIndex + 1) + } + } + return ( + <> +
+
+ {currentIndex < fixtures.length - 1 ? ( + + ) : ( +
+ )} +
+ Gameweek + + {selectedFixture ? selectedFixture.gameWeek : ""} + +
+ {currentIndex > 0 ? ( + + ) : ( +
+ )} +
+ {selectedFixture && ( + + queryKey={["fixtureGames", selectedFixture.slug]} + queryFn={() => GetFixtureGames(selectedFixture.slug)} + refetchInterval={10000} + enabled={!!selectedFixture} + > + {(data) => } + + )} +
+ + ) +} + +function areDatesOnSameDay(date1: Date, date2: Date) { + return ( + date1.getFullYear() === date2.getFullYear() && + date1.getMonth() === date2.getMonth() && + date1.getDate() === date2.getDate() + ) +} + +type TranslationKey = "today" | "tomorrow" | "yesterday" +type LanguageCode = + | "en" + | "zh" + | "hi" + | "es" + | "ar" + | "bn" + | "pt" + | "ru" + | "ja" + | "pa" + | "jv" + | "de" + | "fr" + | "it" + | "tr" + | "ko" + | "fa" + | "uk" + | "pl" + | "ro" + | "nl" + | "th" + | "el" + | "sv" + | "cs" +type Translations = Record> +function getLocalizedWord(word: TranslationKey): string { + const translations: Translations = { + en: { today: "Today", tomorrow: "Tomorrow", yesterday: "Yesterday" }, + zh: { today: "今天", tomorrow: "明天", yesterday: "昨天" }, + hi: { today: "आज", tomorrow: "कल", yesterday: "कल" }, + es: { today: "Hoy", tomorrow: "Mañana", yesterday: "Ayer" }, + ar: { today: "اليوم", tomorrow: "غدًا", yesterday: "أمس" }, + bn: { today: "আজ", tomorrow: "আগামীকাল", yesterday: "গতকাল" }, + pt: { today: "Hoje", tomorrow: "Amanhã", yesterday: "Ontem" }, + ru: { today: "Сегодня", tomorrow: "Завтра", yesterday: "Вчера" }, + ja: { today: "今日", tomorrow: "明日", yesterday: "昨日" }, + pa: { today: "ਅੱਜ", tomorrow: "ਕਲ", yesterday: "ਕੱਲ੍ਹ" }, + jv: { today: "Dina iki", tomorrow: "Esuk", yesterday: "Wingi" }, + de: { today: "Heute", tomorrow: "Morgen", yesterday: "Gestern" }, + fr: { today: "Aujourd'hui", tomorrow: "Demain", yesterday: "Hier" }, + it: { today: "Oggi", tomorrow: "Domani", yesterday: "Ieri" }, + tr: { today: "Bugün", tomorrow: "Yarın", yesterday: "Dün" }, + ko: { today: "오늘", tomorrow: "내일", yesterday: "어제" }, + fa: { today: "امروز", tomorrow: "فردا", yesterday: "دیروز" }, + uk: { today: "Сьогодні", tomorrow: "Завтра", yesterday: "Вчора" }, + pl: { today: "Dziś", tomorrow: "Jutro", yesterday: "Wczoraj" }, + ro: { today: "Astăzi", tomorrow: "Mâine", yesterday: "Ieri" }, + nl: { today: "Vandaag", tomorrow: "Morgen", yesterday: "Gisteren" }, + th: { today: "วันนี้", tomorrow: "พรุ่งนี้", yesterday: "เมื่อวาน" }, + el: { today: "Σήμερα", tomorrow: "Αύριο", yesterday: "Χθες" }, + sv: { today: "Idag", tomorrow: "Imorgon", yesterday: "Igår" }, + cs: { today: "Dnes", tomorrow: "Zítra", yesterday: "Včera" }, + } + + const lang = navigator.language.slice(0, 2) + if ((translations as Record)[lang]) { + return translations[lang as LanguageCode][word] || word + } + return word +} + +function getDayString(day: Date) { + const today = new Date() + const todayDate = today.getDate() + const dayDate = day.getDate() + + if (areDatesOnSameDay(day, today)) { + return getLocalizedWord("today") + } + if ( + dayDate === todayDate + 1 && + day.getMonth() === today.getMonth() && + day.getFullYear() === today.getFullYear() + ) { + return getLocalizedWord("tomorrow") + } + if ( + dayDate === todayDate - 1 && + day.getMonth() === today.getMonth() && + day.getFullYear() === today.getFullYear() + ) { + return getLocalizedWord("yesterday") + } + + const dayName = new Intl.DateTimeFormat(navigator.language, { + weekday: "long", + }).format(day) + return dayName.charAt(0).toUpperCase() + dayName.slice(1) +} + +function FixtureGames({ fixture, games }: { fixture: Fixture; games: Game[] }) { + const [showOnlyStartedAndScheduled, setShowOnlyStartedAndScheduled] = + useState(false) + const toggleFilter = () => { + setShowOnlyStartedAndScheduled((prevState) => !prevState) + } + + const [selectedDay, setSelectedDay] = useState(new Date()) + useEffect(() => { + let initialDate = new Date() + if ( + initialDate > new Date(fixture.endDate) || + initialDate < new Date(fixture.startDate) + ) { + initialDate = new Date(fixture.startDate) + } + initialDate.setHours(0, 0, 0, 0) + setSelectedDay(initialDate) + }, [fixture]) + + const uniqueDays = useMemo(() => { + const daysSet = new Set() + + games?.forEach((game) => { + const d = new Date(game.date) + const dateString = `${d.getFullYear()}-${String( + d.getMonth() + 1 + ).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}` + daysSet.add(dateString) + }) + + return Array.from(daysSet).map((dateString) => new Date(dateString)) + }, [games]) + + const filteredGames = useMemo(() => { + let res = games?.filter((game: Game) => + areDatesOnSameDay(new Date(game.date), selectedDay) + ) + if (showOnlyStartedAndScheduled) { + res = res?.filter( + (game: Game) => game.status === "playing" || game.status === "scheduled" + ) + } + return res?.sort((a: Game, b: Game) => { + const time1 = new Date(a.date).getTime() + const time2 = new Date(b.date).getTime() + if (time1 === time2) { + return a.id > b.id ? 1 : -1 + } + return time1 - time2 + }) + }, [games, showOnlyStartedAndScheduled, selectedDay]) + + return ( +
+
+ {uniqueDays + .sort((a: Date, b: Date) => a.getTime() - b.getTime()) + .map((day) => ( +
+ +
+ ))} +
+
+ + +
+
+ {filteredGames.map((game) => ( + <> +
+ {game.status === "playing" ? ( + game.periodType === "HALF_TIME" ? ( + HT + ) : ( + {game.minutes}' + ) + ) : game.status === "played" ? ( + FT + ) : game.status === "scheduled" ? ( + + {new Date(game.date).toLocaleString(navigator.language, { + hour: "numeric", + minute: "numeric", + })} + + ) : null} +
+
+ {game.homeTeam.shortName} +
+ +
+
+
+
+
+ {game.homePenaltyScore > 0 || game.awayPenaltyScore > 0 ? ( + ({game.homePenaltyScore}) + ) : null} + + {game.status !== "scheduled" ? game.homeGoals : "-"} + +
+
+ {game.status !== "scheduled" && -} +
+
+ + {game.status !== "scheduled" ? game.awayGoals : "-"} + + {game.awayPenaltyScore > 0 || game.homePenaltyScore > 0 ? ( + ({game.awayPenaltyScore}) + ) : null} +
+
+
+
+
+
+ +
+ {game.awayTeam.shortName} +
+
+ +
+
+ {game.competition.displayName} +
+
+ {`${game.competition.displayName} +
+
+ + ))} +
+
+ ) +} diff --git a/front/src/pages/player/club_history.tsx b/front/src/pages/player/club_history.tsx new file mode 100644 index 0000000..ed5d09e --- /dev/null +++ b/front/src/pages/player/club_history.tsx @@ -0,0 +1,79 @@ +import { useMemo } from "react" +import Player from "../../types/player" + +export default function ClubHistory({ player }: { player: Player }) { + const clubMemberships = useMemo( + () => + player.clubMembership + .filter((membership) => membership.membershipType === "club") + .sort((a, b) => { + if (!a.endDate) return -1 + if (!b.endDate) return 1 + if (new Date(a.endDate).getTime() === new Date(b.endDate).getTime()) { + return ( + new Date(b.startDate).getTime() - new Date(a.startDate).getTime() + ) + } + return new Date(b.endDate).getTime() - new Date(a.endDate).getTime() + }), + [player.clubMembership] + ) + return ( +
+ {clubMemberships.map((membership) => ( + <> +
+ +
+
+ +
+ +
{membership.team?.displayName}
+
+ + {new Date(membership.startDate).toLocaleDateString(undefined, { + month: "numeric", + year: "numeric", + })} + + {membership.endDate ? "->" : ""} + + {membership.endDate + ? new Date(membership.endDate).toLocaleDateString(undefined, { + month: "numeric", + year: "numeric", + }) + : ""} + +
+
+ {(() => { + const endDate = membership.endDate + ? new Date(membership.endDate) + : new Date() + const startDate = new Date(membership.startDate) + const diffDays = Math.floor( + (endDate.getTime() - startDate.getTime()) / + (1000 * 60 * 60 * 24) + ) + const diffYears = Math.floor(diffDays / 365) + const remainingDays = diffDays % 365 + if (diffYears > 1) { + return `${diffYears} years ${remainingDays} days` + } else if (diffYears === 1) { + return `1 year ${remainingDays} days` + } else { + return `${diffDays} days` + } + })()} +
+ + ))} +
+ ) +} diff --git a/front/src/pages/player/components/card_icon.tsx b/front/src/pages/player/components/card_icon.tsx new file mode 100644 index 0000000..735a428 --- /dev/null +++ b/front/src/pages/player/components/card_icon.tsx @@ -0,0 +1,25 @@ +import React from "react" + +interface CardIconProps { + size?: number + color?: string +} + +const CardIcon: React.FC = ({ + size = 16, + color = "currentColor", +}) => { + return ( + + + + ) +} + +export default CardIcon diff --git a/front/src/pages/player/components/days_selection.tsx b/front/src/pages/player/components/days_selection.tsx new file mode 100644 index 0000000..7b8cf57 --- /dev/null +++ b/front/src/pages/player/components/days_selection.tsx @@ -0,0 +1,117 @@ +import { useState } from "react" + +function extractDateWithOffset(date: Date, offsetDays: number) { + const year = date.getFullYear() + const month = date.getMonth() + const day = date.getDate() + + const resultDate = new Date(year, month, day) + resultDate.setDate(resultDate.getDate() + offsetDays) + + return resultDate +} + +const CustomRadioButton = ({ + value, + currentValue, + onClick, + label, +}: { + value: string + currentValue: string + onClick: (value: string) => void + label: string +}) => { + const isActive = value === currentValue + const bg = isActive ? "bg-primary-blue-500" : "bg-neutral-700" + const hoverBg = isActive + ? "hover:bg-primary-blue-600" + : "hover:bg-neutral-600" + const activeBg = isActive + ? "active:bg-primary-blue-700" + : "active:bg-neutral-500" + + return ( + + ) +} + +function DaysSelector({ + days, + setDays, +}: { + days: number + setDays: (days: number) => void +}) { + return ( +
+ setDays(Number(value))} + label="3 days" + /> + setDays(Number(value))} + label="1 week" + /> + setDays(Number(value))} + label="1 month" + /> + setDays(Number(value))} + label="3 months" + /> + setDays(Number(value))} + label="6 months" + /> + setDays(Number(value))} + label="1 year" + /> + setDays(Number(value))} + label="All" + /> +
+ ) +} + +export default function DaysSelectionWrapper({ + children, +}: { + children: (startDate: Date, endDate: Date) => React.ReactNode +}) { + const [days, setDays] = useState(365) + const farEnoughDate = new Date(2000, 0, 1) // You can set this to an earlier date if needed + const startDate = + days === 0 ? farEnoughDate : extractDateWithOffset(new Date(), -days) + const endDate = extractDateWithOffset(new Date(), 1) + + return ( +
+ +
{children(startDate, endDate)}
+
+ ) +} diff --git a/front/src/pages/player/components/filter_select.tsx b/front/src/pages/player/components/filter_select.tsx new file mode 100644 index 0000000..490bdd7 --- /dev/null +++ b/front/src/pages/player/components/filter_select.tsx @@ -0,0 +1,65 @@ +import Select, { MultiValue, SingleValue } from "react-select" + +interface FilterOption { + value: T + label: string +} + +interface FilterSelectProps { + value: T + options: FilterOption[] + onChange: (value: T) => void + isSearchable?: boolean +} + +export function FilterSelect({ + value, + options, + onChange, + isSearchable = false, +}: FilterSelectProps) { + return ( + ) => + values.includes(option.value) + )} + isMulti + isSearchable={isSearchable} + options={options} + placeholder={placeHolder} + isDisabled={options.length === 0} + onChange={(options: MultiValue>) => { + onChange(options.map((option: FilterOption) => option.value) as T[]) + }} + /> + ) +} diff --git a/front/src/pages/player/components/scorebox.tsx b/front/src/pages/player/components/scorebox.tsx new file mode 100644 index 0000000..9055afd --- /dev/null +++ b/front/src/pages/player/components/scorebox.tsx @@ -0,0 +1,23 @@ +import { GetScoreColor } from "../../../types/game" + +export default function ScoreBox({ + score, + colorfunc, +}: { + score: number + colorfunc?: (score: number, tailwind: boolean) => string +}) { + if (!colorfunc) colorfunc = GetScoreColor + return ( +
+ + {Number(score).toFixed(0)} + +
+ ) +} diff --git a/front/src/pages/player/index.tsx b/front/src/pages/player/index.tsx new file mode 100644 index 0000000..6995528 --- /dev/null +++ b/front/src/pages/player/index.tsx @@ -0,0 +1,56 @@ +import { useState } from "react" +import { useParams } from "react-router" +import { GetPlayer } from "../../api/player" +import WithDataFetching from "../../components/withdatafetching" +import Player from "../../types/player" +import ClubHistory from "./club_history" +import Scores from "./scores" +import Sidebar from "./sidebar" + +export default function PlayerPage() { + const { slug } = useParams() + + if (!slug) { + return
No slug
+ } + return ( + GetPlayer(slug)} + > + {(player) => } + + ) + + function PlayerLayout({ player }: { player: Player }) { + const [activeTab, setActiveTab] = useState("club_history") + const tabs = [ + { id: "club_history", title: "Club History" }, + { id: "scores", title: "Scores" }, + ] + return ( +
+ +
+
+ {tabs.map((tab) => ( + + ))} +
+
+ {activeTab === "club_history" && ( + + )} + {activeTab === "scores" && } +
+
+
+ ) + } +} diff --git a/front/src/pages/player/score_graph.tsx b/front/src/pages/player/score_graph.tsx new file mode 100644 index 0000000..8a14f6a --- /dev/null +++ b/front/src/pages/player/score_graph.tsx @@ -0,0 +1,184 @@ +import { + addMonths, + format, + isSameDay, + startOfMonth, + startOfYear, + subMonths, +} from "date-fns" +import moment from "moment/moment" +import { useMemo } from "react" +import { + Cell, + ComposedChart, + LabelList, + ReferenceLine, + ResponsiveContainer, + Scatter, + Tooltip, + XAxis, + YAxis, +} from "recharts" +import { + Game, + GamePlayer, + GamePlayerScore, + GetScoreColor, +} from "../../types/game" +import Player from "../../types/player" + +type DataPoint = { + date: Date + matchScore?: number + decisiveScore?: number + aaScore?: number + game?: Game + score?: GamePlayerScore + gameweek: number + dateAsNumber: number +} + +const CustomTooltip = ({ active, payload }: any) => { + if (active && payload && payload.length) { + const dataPoint = payload[0].payload + + const { gameweek, date } = dataPoint + const formattedDate = moment(date).format("DD MMM YYYY") + + return ( +
+

{`Date: ${formattedDate}`}

+ {gameweek &&

{`Gameweek: ${gameweek}`}

} + {payload.map((item: any) => ( +

{`${item.name}: ${Math.round(item.value)}`}

+ ))} +
+ ) + } + + return null +} + +export default function ScoreGraph({ + scores, + startDate, + endDate, +}: { + player: Player + scores: GamePlayer[] + startDate: Date + endDate: Date +}) { + const CustomXAxisTick = ({ x, y, payload }: any) => { + const date = new Date(payload.value) + const isFirstJan = isSameDay(date, startOfYear(date)) + const isFirstOfMonth = isSameDay(date, startOfMonth(date)) + const displayText = isFirstJan + ? format(date, "yyyy") + : isFirstOfMonth + ? format(date, "MMM") + : "" + + return ( + + + {displayText} + + + ) + } + + const adjustedStartDate = useMemo(() => { + const earliestScoreDate = scores.reduce((minDate, score) => { + const scoreDate = new Date(score.game?.date ?? "") + return minDate > scoreDate ? scoreDate : minDate + }, new Date()) + return startDate > earliestScoreDate ? startDate : earliestScoreDate + }, [scores, startDate]) + + const filteredScores = useMemo( + () => + scores + .filter( + (score) => + new Date(score.game?.date ?? "") >= startDate && + new Date(score.game?.date ?? "") <= endDate + ) + .map((score) => ({ + date: score.game?.fixture.startDate ?? new Date(), + matchScore: score.score?.score, + decisiveScore: score.score?.decisiveScore, + aaScore: score.score?.allAroundScore, + game: score.game, + score: score.score, + gameweek: score.game?.fixture.gameWeek ?? 0, + dateAsNumber: + new Date(score.game?.fixture.startDate ?? new Date()).getTime() ?? + 0, + })) + .sort((a, b) => a.gameweek - b.gameweek), + [scores, startDate, endDate] + ) + + const ticks = useMemo(() => { + const start = startOfMonth(adjustedStartDate) + const end = startOfMonth(endDate) + let current = start + const dates = [] + while (current <= end) { + dates.push(current.getTime()) + current = addMonths(current, 1) + } + dates.unshift(subMonths(start, 1).getTime()) + dates.push(addMonths(end, 1).getTime()) + return dates + }, [adjustedStartDate, endDate]) + + return ( +
+ + + } + ticks={ticks} + /> + + + {filteredScores?.map((entry, index) => ( + + ))} + Math.round(value)} + /> + + {[0, 20, 40, 60, 80, 100].map((value) => ( + + ))} + + } /> + + +
+ ) +} diff --git a/front/src/pages/player/score_table.tsx b/front/src/pages/player/score_table.tsx new file mode 100644 index 0000000..424181c --- /dev/null +++ b/front/src/pages/player/score_table.tsx @@ -0,0 +1,363 @@ +import moment from "moment" +import React from "react" +import { BiFootball } from "react-icons/bi" +import { BsHandThumbsUp } from "react-icons/bs" +import { FaHandSparkles } from "react-icons/fa" +import { GiFist, GiFootprint, GiLeg, GiWhistle } from "react-icons/gi" +import { TbHandStop } from "react-icons/tb" +import { + GamePlayer, + GetAAScoreColor, + GetDSScoreColor, + GetMinutesScoreColor, + GetScoreColor, +} from "../../types/game" +import Player from "../../types/player" +import CardIcon from "./components/card_icon" +import { FilterSelect } from "./components/filter_select" +import ScoreBox from "./components/scorebox" + +type GameEventProps = { + count: number | undefined + icon: React.ElementType + positive?: boolean + label?: string +} + +function GameEvent({ + count, + icon: Icon, + positive = true, + label = "", +}: GameEventProps) { + if (!count || count === 0) { + return null + } + + return ( +
+ {count} +
+ +
+
+ ) +} + +function ScoreText({ score }: { score: number | string }) { + return ( + + {score} + + ) +} + +function ScoreHeader({ label, tooltip }: { label: string; tooltip: string }) { + return ( + + {label} + + ) +} + +export default function ScoreTable({ + player, + scores, +}: { + player: Player + scores: GamePlayer[] +}) { + const [statCategory, setStatCategory] = React.useState("def") + + let availableStatCategory = [ + { value: "def", label: "Defensive" }, + { value: "off", label: "Offensive" }, + { value: "poss", label: "Possession" }, + ] + if (player.fieldPosition === "Goalkeeper") { + availableStatCategory = [ + ...availableStatCategory, + { value: "goal", label: "Goalkeeping" }, + ] + } + + return ( +
+
+ +
+ + + + + + + + + + + + + + + + + {statCategory === "def" && ( + <> + + + + + + + + + )} + {statCategory === "off" && ( + <> + + + + + + + + + )} + {statCategory === "poss" && ( + <> + + + + + + + + + + + )} + {statCategory === "goal" && ( + <> + + + + + + + + + + + )} + + + + {scores + ?.sort((a, b) => + new Date(a.game?.date ?? new Date()) < + new Date(b.game?.date ?? new Date()) + ? 1 + : -1 + ) + .map((scoreData) => { + if (!scoreData.game || !scoreData.score) return null + return ( + + + + + + + + + + + + + + + {statCategory === "def" && + (scoreData.score.minutesPlayed ?? 0) > 0 && ( + <> + + + + + + + + + )} + {statCategory === "off" && + scoreData.score.minutesPlayed > 0 && ( + <> + + + + + + + + + )} + {statCategory === "poss" && + scoreData.score.minutesPlayed > 0 && ( + <> + + + + + + + + + + + )} + {statCategory === "goal" && + scoreData.score.minutesPlayed > 0 && ( + <> + + + + + + + + + + + )} + + ) + })} + +
+ GW + Date + Minutes + HomeAwayScore + DS + + AA +
{scoreData.game.fixture?.gameWeek}{moment(scoreData.game.date).format("DD MMMM YYYY")} + + {scoreData.score.minutesPlayed} + + + + {scoreData.game.homeTeam.displayName} + + {scoreData.game.homeGoals}-{scoreData.game.awayGoals} + + {scoreData.game.awayTeam.displayName} + + + + +
+ + + + + + + + } + label="Red card" + /> + } + label="Yellow card" + /> +
+
+ + + + + {scoreData.score.cleanSheet ? : ""} +
+
+ ) +} diff --git a/front/src/pages/player/scores.tsx b/front/src/pages/player/scores.tsx new file mode 100644 index 0000000..9249bb8 --- /dev/null +++ b/front/src/pages/player/scores.tsx @@ -0,0 +1,36 @@ +import { GetPlayerGames } from "../../api/player" +import WithDataFetching from "../../components/withdatafetching" +import Player from "../../types/player" +import DaysSelectionWrapper from "./components/days_selection" +import ScoreGraph from "./score_graph" +import ScoreTable from "./score_table" + +export default function Scores({ player }: { player: Player }) { + return ( + + {(startDate, endDate) => { + return ( + GetPlayerGames(player.slug)} + refetchOnWindowFocus={false} + > + {(scores) => { + return ( +
+ + +
+ ) + }} +
+ ) + }} +
+ ) +} diff --git a/front/src/pages/player/sidebar.tsx b/front/src/pages/player/sidebar.tsx new file mode 100644 index 0000000..e36c056 --- /dev/null +++ b/front/src/pages/player/sidebar.tsx @@ -0,0 +1,57 @@ +import Player from "../../types/player" + +function Badge({ + imgUrl, + name, + imgHeight = "full", +}: { + imgUrl?: string + name?: string + imgHeight?: string +}) { + return ( +
+ + {name} +
+ ) +} + +export default function Sidebar({ player }: { player: Player }) { + return ( +
+
+ +
+ + + {player.displayName} + +
+ + {new Date().getFullYear() - new Date(player.birthDate).getFullYear()} + + + {new Date(player.birthDate).toLocaleDateString("en-GB", { + year: "numeric", + month: "2-digit", + day: "2-digit", + })} + +
+ + + +
+ ) +} diff --git a/front/src/pages/singlerankings/competition_select.tsx b/front/src/pages/singlerankings/competition_select.tsx new file mode 100644 index 0000000..8abe2fb --- /dev/null +++ b/front/src/pages/singlerankings/competition_select.tsx @@ -0,0 +1,136 @@ +import { useMemo } from "react" +import { SingleRankingsParams } from "../../api/rankings" +import InputWrapper from "../../components/input_wrapper" +import Competition from "../../types/competition" + +interface ZoneGroup { + label: string + options: Array<{ label: string; value: string }> +} + +interface CompetitionsSelectProps { + competitions: Competition[] + params: SingleRankingsParams + setParams: (params: SingleRankingsParams) => void +} + +function CompetitionsSelect({ + competitions, + params, + setParams, +}: CompetitionsSelectProps) { + const zoneGroups = useMemo(() => { + const groups: Record = {} + competitions.forEach((competition) => { + if (!competition.zone) return + if (!groups[competition.zone.id]) { + groups[competition.zone.id] = { + label: competition.zone?.displayName, + options: [], + } + } + groups[competition.zone.id].options.push({ + label: competition.displayName, + value: competition.slug, + }) + }) + return groups + }, [competitions]) + + const groupedOptions = useMemo(() => { + const sortedGroups = Object.values(zoneGroups).sort((a, b) => { + if (a.label === "") return 1 + if (b.label === "") return -1 + return a.label.localeCompare(b.label) + }) + + const withoutZoneGroup = { + label: "Without zone", + options: sortedGroups.find((group) => group.label === "")?.options || [], + } + + const filteredGroups = sortedGroups + .filter((group) => group.label !== "") + .map((group) => ({ + ...group, + options: group.options.sort((a, b) => a.label.localeCompare(b.label)), + })) + + if (withoutZoneGroup.options.length > 0) { + filteredGroups.push(withoutZoneGroup) + } + + return filteredGroups + }, [zoneGroups]) + + const selectedOptions = useMemo(() => { + return params.competitions.map((slug) => { + const competition = competitions.find((c) => c.slug === slug) + return { value: slug, label: competition?.displayName || "" } + }) + }, [competitions, params.competitions]) + + return ( + + + + {/* { + if (e.target.value) { + setParams({ + ...params, + startDate: new Date(e.target.value), + }) + } + }} + /> + + + { + setParams({ + ...params, + minGameCount: value, + }) + }} + /> + + { + setParams({ + ...params, + minTeamGamesPlayedPercentage: value, + }) + }} + /> + +
+ +
+ + { + setParams({ + ...params, + minAge: value, + }) + }} + /> + { + setParams({ + ...params, + maxAge: value, + }) + }} + /> + + { + setParams({ + ...params, + minTotalMinutes: value, + }) + }} + /> + + + +
+ ) => { + setParams({ + ...params, + onlyClubGames: e.target.checked, + }) + }} + /> + ) => { + setParams({ + ...params, + onlyStarting: e.target.checked, + }) + }} + /> + ) => { + setParams({ + ...params, + u23: e.target.checked, + }) + }} + /> + ) => { + setParams({ + ...params, + hasGameInNextGw: e.target.checked, + }) + }} + /> +
+
+ { + setParams({ + ...params, + limit: value, + }) + }} + /> + + + + +
+ + {isFetching ? ( + + ) : ( +
+ + + + + + + + + + + + + + {["Goalkeeper", "Defender"].includes(usedParams.position) && ( + + )} + {["Midfielder", "Forward"].includes(usedParams.position) && ( + + )} + + + + + + + + {data?.map((item) => { + return ( + + + + + + + + + + + + {["Goalkeeper", "Defender"].includes( + usedParams.position + ) && ( + + )} + {["Midfielder", "Forward"].includes( + usedParams.position + ) && ( + + )} + + + + + + ) + })} + +
+ Team + + Name + + Age + + Score + + Stddev + + DS + + AA + + Games Played + + % Minutes Played + + Avg Team GA + + Avg Team GF + + AA/Min + + Total Min + +
+ Score Percentages +
+ + {">80"} + + + {">70"} + + + {">60"} + + + {">50"} + +
+
+
+
+ AA Percentages + + {">40"} + + + {">30"} + + + {">20"} + + + {">10"} + +
+
+
+ + {""} + +
+
+ + + + {item.player.displayName} + + +
+ + {calculateAge(new Date(item.player.birthDate))} + +
+
+
+
+ + {item.minScore} + + + {item.totalAvgScore} + + + {item.maxScore} + +
+
+ {item.scores + .slice(0, 5) + .reverse() + .map((score, index) => ( + 0 ? "border-l border-neutral-400" : ""} w-full px-1 py-1 text-center + ${score >= 80 ? "bg-green-600" : score >= 60 ? "bg-green-500" : score >= 40 ? "bg-yellow-400" : score >= 20 ? "bg-orange-500" : "bg-red-500"} + ${index === 0 ? "rounded-l-lg" : index === 4 ? "rounded-r-lg" : ""}`} + > + {Number(score) === -1 + ? "x" + : Number(score).toFixed(0)} + + ))} +
+
+
+ ±{item.totalStddevScore} + + {item.decisiveAvgScore} + + {item.allAroundAvgScore} + +
+
+ +
+
+ + {item.gameBenchedUnplayedCount} + + + {item.gameBenchedPlayedCount} + + + {item.gameStartedCount} + + + {item.teamsGameCount} + +
+
+
+ {item.percentageMinutesPlayed} + + {item.avgTeamGoalsAgainst} + + {item.avgTeamGoalsFor} + + {item.aaPerMin} + + {item.totalMinutes} + + + + +
+
+ )} + + ) +} + +export default SingleRankingsPage diff --git a/front/src/pages/singlerankings/position_select.tsx b/front/src/pages/singlerankings/position_select.tsx new file mode 100644 index 0000000..6e81cf9 --- /dev/null +++ b/front/src/pages/singlerankings/position_select.tsx @@ -0,0 +1,42 @@ +import { SingleRankingsParams } from "../../api/rankings"; + +const positions = ["Goalkeeper", "Defender", "Midfielder", "Forward"]; + +interface PositionSelectProps { + params: SingleRankingsParams; + setParams: (params: SingleRankingsParams) => void; +} + +export default function PositionSelect({ + params, + setParams, +}: PositionSelectProps) { + const handlePositionChange = (position: string) => { + setParams({ + ...params, + position, + }); + }; + + return ( +
+ {positions.map((position, index) => ( +
0 ? "border-l-2 border-gray-300" : ""}`} + > + +
+ ))} +
+ ); +} diff --git a/front/src/pages/singlerankings/zone_select.tsx b/front/src/pages/singlerankings/zone_select.tsx new file mode 100644 index 0000000..f37ba69 --- /dev/null +++ b/front/src/pages/singlerankings/zone_select.tsx @@ -0,0 +1,51 @@ +import { useQuery } from "@tanstack/react-query"; +import { SingleRankingsParams } from "../../api/rankings"; +import { GetAllZones } from "../../api/zone"; +import Zone from "../../types/zone"; + +interface ZoneSelectProps { + params: SingleRankingsParams; + setParams: (params: SingleRankingsParams) => void; +} + +export default function ZoneSelect({ params, setParams }: ZoneSelectProps) { + const { data: zones } = useQuery({ + queryKey: ["zones"], + queryFn: async () => { + const zones = await GetAllZones(); + return zones.sort((a, b) => a.displayName.localeCompare(b.displayName)); + }, + }); + + const handleZoneChange = (zone: Zone) => { + const newZones = params.zones.includes(zone.id) + ? params.zones.filter((z) => z !== zone.id) + : [...params.zones, zone.id]; + setParams({ + ...params, + zones: newZones, + }); + }; + + return ( +
+ {zones?.map((zone, index) => ( +
0 ? "border-l-2 border-gray-300" : ""}`} + > + +
+ ))} +
+ ); +} diff --git a/front/src/pages/xpcenter/index.tsx b/front/src/pages/xpcenter/index.tsx new file mode 100644 index 0000000..b2c9969 --- /dev/null +++ b/front/src/pages/xpcenter/index.tsx @@ -0,0 +1,205 @@ +import { useMemo, useState } from "react" +import { useParams } from "react-router" +import { Link } from "react-router-dom" +import { GetUserCards } from "../../api/card" +import WithDataFetching from "../../components/withdatafetching" +import Card from "../../types/card" + +export default function XpCenter() { + const { slug = "gigiz22" } = useParams() + + return ( + + queryKey={["user", slug, "cards"]} + queryFn={() => GetUserCards(slug)} + refetchOnWindowFocus={false} + > + {(data) => } + + ) +} + +function CardList({ cards }: { cards: Card[] }) { + const [sortConfig, setSortConfig] = useState<{ + key: string + direction: "asc" | "desc" + } | null>(null) + + const [hideMaxLevelUp, setHideMaxLevelUp] = useState(false) + + const filteredCards = useMemo(() => { + return hideMaxLevelUp + ? cards.filter( + (card) => card.levelUpAppliedCount < card.maxLevelUpAppliedCount + ) + : cards + }, [cards, hideMaxLevelUp]) + + const sortedCards = useMemo(() => { + let sortableCards = filteredCards.map((card) => ({ + ...card, + xpNeeded: + card.xpNeededForNextGrade > card.xp + ? card.xpNeededForNextGrade - card.xp + : "N/A", + xpPercentage: + card.xpNeededForNextGrade > card.xp + ? ((card.xpNeededForNextGrade - card.xp) * 100) / + (card.xpNeededForNextGrade - card.xpNeededForCurrentGrade) + : "N/A", + })) + + if (sortConfig !== null) { + sortableCards.sort((a, b) => { + const key = sortConfig.key as keyof typeof a + if (a[key] < b[key]) { + return sortConfig.direction === "asc" ? -1 : 1 + } + if (a[key] > b[key]) { + return sortConfig.direction === "asc" ? 1 : -1 + } + return 0 + }) + } + return sortableCards + }, [filteredCards, sortConfig]) + + const requestSort = (key: string) => { + let direction: "asc" | "desc" = "asc" + if ( + sortConfig && + sortConfig.key === key && + sortConfig.direction === "asc" + ) { + direction = "desc" + } + setSortConfig({ key, direction }) + } + + const getSortIndicator = (key: string) => { + if (!sortConfig || sortConfig.key !== key) return null + return sortConfig.direction === "asc" ? "↑" : "↓" + } + + return ( + <> +
+ +
+ + + + + + + + + + + + + + {sortedCards.map((card) => ( + + + + + + + + + + ))} + +
+ Player + requestSort("grade")} + > + Grade{" "} + + {getSortIndicator("grade")} + + requestSort("xp")} + > + XP{" "} + + {getSortIndicator("xp")} + + requestSort("xpNeeded")} + > + XP Needed for Next Grade{" "} + + {getSortIndicator("xpNeeded")} + + requestSort("xpPercentage")} + > + % Until next grade{" "} + + {getSortIndicator("xpPercentage")} + + + Level up applied +
+ + {card.name} + + + +
+
+ {card.playerDisplayName} +
+
+ +
+
{card.grade}
+
+
{card.xp}
+
+
{card.xpNeeded}
+
+
+ {card.xpPercentage !== "N/A" + ? `${Number(card.xpPercentage).toFixed(0)}%` + : "N/A"} +
+
+
+ {card.levelUpAppliedCount} / {card.maxLevelUpAppliedCount} +
+
+ + ) +} diff --git a/front/src/router/router.tsx b/front/src/router/router.tsx new file mode 100644 index 0000000..c7f8ede --- /dev/null +++ b/front/src/router/router.tsx @@ -0,0 +1,50 @@ +import { createBrowserRouter, Link, RouterProvider } from "react-router-dom" +import Layout from "../pages/layout" +import Live from "../pages/live" +import PlayerPage from "../pages/player" +import SingleRankingsPage from "../pages/singlerankings" +import XpCenter from "../pages/xpcenter" + +const router = createBrowserRouter([ + { + path: "/", + element: , + children: [ + { path: "/", element: }, + { path: "/live", element: }, + { path: "/xpcenter", element: }, + { path: "/xpcenter/:slug", element: }, + { path: "/player/:slug", element: }, + ], + }, + { path: "*", element: }, +]) + +export default function RouterOutlet() { + return +} + +function NoMatch() { + return ( +
+
+

+ 404 +

+

+ Page Not Found +

+

+ The page you are looking for doesn't exist or has been moved. +

+ +
+ +
+ +
+
+ ) +} diff --git a/front/src/types/card.ts b/front/src/types/card.ts new file mode 100644 index 0000000..c1f5400 --- /dev/null +++ b/front/src/types/card.ts @@ -0,0 +1,27 @@ +export default interface Card { + id: string + assetId: string + playerSlug: string + playerDisplayName: string + rarity: string + serialNumber: number + seasonStartYear: number + singleCivilYear: boolean + supply: number + teamSlug: string + name: string + pictureUrl: string + slug: string + power: number + powerMalusAfterTransfer: number + rivalsPower: number + grade: number + gradeAfterTransfer: number + xp: number + xpAfterTransfer: number + xpNeededForNextGrade: number + xpNeededForCurrentGrade: number + inSeasonEligible: boolean + levelUpAppliedCount: number + maxLevelUpAppliedCount: number +} diff --git a/front/src/types/competition.ts b/front/src/types/competition.ts new file mode 100644 index 0000000..4e33d8e --- /dev/null +++ b/front/src/types/competition.ts @@ -0,0 +1,15 @@ +import Country from "./country" +import Zone from "./zone" + +export default interface Competition { + slug: string + displayName: string + countrySlug: string + competitionFormat: string + competitionType: string + pictureUrl: string + logoUrl: string + zoneId?: number + zone?: Zone + country: Country +} diff --git a/front/src/types/country.ts b/front/src/types/country.ts new file mode 100644 index 0000000..85f6cc7 --- /dev/null +++ b/front/src/types/country.ts @@ -0,0 +1,11 @@ +export default interface Country { + slug: string; + code: string; + displayName: string; + threeLetterCode: string; + flagFlat64Url: string; + flagFlat32Url: string; + flagRound64Url: string; + flagRound32Url: string; +} + diff --git a/front/src/types/fixture.ts b/front/src/types/fixture.ts new file mode 100644 index 0000000..9294c3c --- /dev/null +++ b/front/src/types/fixture.ts @@ -0,0 +1,8 @@ +export interface Fixture { + slug: string + displayName: string + fixtureState: string + startDate: Date + endDate: Date + gameWeek: number +} diff --git a/front/src/types/game.ts b/front/src/types/game.ts new file mode 100644 index 0000000..898e95d --- /dev/null +++ b/front/src/types/game.ts @@ -0,0 +1,177 @@ +import Competition from "./competition" +import { Fixture } from "./fixture" +import Player from "./player" +import Team from "./team" + +export interface Game { + id: string + date: Date + coverageStatus: string + lowCoverage: boolean + minutes: number + periodType: string + scored: boolean + status: string + competitionSlug: string + fixtureSlug: string + awayTeamSlug: string + awayGoals: number + awayExtraTimeScore: number + awayPenaltyScore: number + homeTeamSlug: string + homeGoals: number + homeExtraTimeScore: number + homePenaltyScore: number + winnerTeamSlug: string + competition: Competition + homeTeam: Team + awayTeam: Team + winnerTeam: Team + gamePlayers: GamePlayer[] + fixture: Fixture +} + +export interface GamePlayer { + gameId: string + playerSlug: string + teamSlug: string + status: string + + game?: Game + player?: Player + score?: GamePlayerScore +} + +export interface GamePlayerScore { + gameId: string + playerSlug: string + score: number + decisiveScore: number + allAroundScore: number + minutesPlayed: number + gameStarted: boolean + formationPlace: number + live: boolean + onGameSheet: boolean + reviewed: boolean + goal: number + assist: number + penaltyWon: number + clearanceOffLine: number + lastManTackle: number + penaltySave: number + ownGoal: number + redCard: boolean + errorLeadToGoal: number + penaltyConceded: number + yellowCard: number + fouls: number + fouled: number + cleanSheet: boolean + doubleDouble: boolean + tripleDouble: boolean + tripleTriple: boolean + errorLeadToShot: number + saves: number + savedShotFromInsideBox: number + goodHighClaim: number + punches: number + divingSave: number + divingCatch: number + crossNotClaimed: number + goalkeeperSmother: number + sixSecondViolation: number + keeperSweeper: number + goalsConceded: number + effectiveClearance: number + wonTackle: number + blockedCross: number + block: number + possessionLost: number + possessionWon: number + duelLost: number + duelWon: number + interception: number + accuratePass: number + accurateFinalThirdPass: number + accurateLongBall: number + longPassIntoOpposition: number + missedPass: number + shotOnTarget: number + wonContest: number + bigChanceCreated: number + attemptedAssist: number + penaltyAreaEntries: number + penaltyKickMissed: number + bigChanceMissed: number +} + +export function GetScoreColor(score: number, tailwind: boolean): string { + if (score < 15) { + return tailwind ? "bg-support-red-400" : "#FF0000" // red + } else if (score < 40) { + return tailwind ? "bg-orange-400" : "#FFA500" // orange + } else if (score < 60) { + return tailwind ? "bg-yellow-400" : "#FFFF00" // yellow + } else if (score < 75) { + return tailwind ? "bg-green-600" : "#32CD32" // lime green, a bit darker than light green + } else { + return tailwind ? "bg-green-700" : "#006400" // dark green + } +} + +export function GetLScoreColor(score: number): string { + if (score < 40) { + return "bg-support-red-500" + } else if (score < 46) { + return "bg-orange-500" + } else if (score < 54) { + return "bg-yellow-500" + } else if (score < 66) { + return "bg-green-500" + } else { + return "bg-green-600" + } +} + +export function GetDSScoreColor(score: number): string { + if (score < 34) { + return "bg-support-red-500" + } else if (score < 37) { + return "bg-orange-500" + } else if (score < 42) { + return "bg-yellow-500" + } else if (score < 52) { + return "bg-green-500" + } else { + return "bg-green-600" + } +} + +export function GetAAScoreColor(score: number): string { + if (score < 7) { + return "bg-support-red-500" + } else if (score < 10) { + return "bg-orange-500" + } else if (score < 16) { + return "bg-yellow-500" + } else if (score < 25) { + return "bg-green-500" + } else { + return "bg-green-600" + } +} + +export function GetMinutesScoreColor(score: number): string { + if (score < 38) { + return "bg-support-red-500" + } else if (score < 58) { + return "bg-orange-500" + } else if (score < 78) { + return "bg-yellow-500" + } else if (score < 90) { + return "bg-green-500" + } else { + return "bg-green-600" + } +} diff --git a/front/src/types/player.ts b/front/src/types/player.ts new file mode 100644 index 0000000..1e5dd5b --- /dev/null +++ b/front/src/types/player.ts @@ -0,0 +1,41 @@ +import Competition from "./competition" +import Country from "./country" +import Team from "./team" + +interface CardSupply { + playerSlug: string + seasonStartYear: number + limited: number + rare: number + superRare: number + unique: number + lastUpdated: Date +} +interface Membership { + id: string + playerSlug: string + teamSlug: string + startDate: Date + endDate: Date + membershipType: string + + team?: Team +} + +export default interface Player { + slug: string + displayName: string + birthDate: Date + countrySlug: string + teamSlug?: string + domesticLeagueSlug?: string + avatarUrl: string + fieldPosition: string + status: string + shirtNumber: number + country?: Country + team?: Team + domesticLeague?: Competition + cardSupply: CardSupply[] + clubMembership: Membership[] +} diff --git a/front/src/types/team.ts b/front/src/types/team.ts new file mode 100644 index 0000000..09ba348 --- /dev/null +++ b/front/src/types/team.ts @@ -0,0 +1,15 @@ +import Competition from "./competition" +import Country from "./country" + +export default interface Team { + slug: string + displayName: string + countrySlug: string + domesticLeagueSlug?: string + shortName: string + pictureUrl: string + teamType: string + + country?: Country + domesticLeague?: Competition +} diff --git a/front/src/types/zone.ts b/front/src/types/zone.ts new file mode 100644 index 0000000..4285b43 --- /dev/null +++ b/front/src/types/zone.ts @@ -0,0 +1,4 @@ +export default interface Zone { + id: number; + displayName: string; +} \ No newline at end of file diff --git a/front/src/vite-env.d.ts b/front/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/front/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/front/tailwind.config.js b/front/tailwind.config.js new file mode 100644 index 0000000..b7039b0 --- /dev/null +++ b/front/tailwind.config.js @@ -0,0 +1,100 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], + theme: { + extend: { + fontFamily: { + sans: ["Roboto", "sans-serif"], + secondary: ["\"Exo 2 Variable\"", "sans-serif"], + }, + fontSize: { + "2xs": ".625rem", + }, + colors: { + primary: { + '50': '#f3f7f8', + '100': '#e0e9ed', + '200': '#c4d5dd', + '300': '#9bb8c5', + '400': '#6b93a5', + '500': '#4b7083', + '600': '#456375', + '700': '#3c5362', + '800': '#374753', + '900': '#313e48', + '950': '#1d272f', + }, + white: "#f5f2f1", + + // neutral: { + // 50: "#F7F7F7", + // 100: "#E1E1E1", + // 200: "#CFCFCF", + // 300: "#B1B1B1", + // 400: "#9E9E9E", + // 500: "#7E7E7E", + // 600: "#626262", + // 700: "#515151", + // 800: "#3B3B3B", + // 900: "#222222", + // }, + //white: "neutral.50", + black: "neutral.900", + "primary-yellow": { + 50: "#FFFBEA", + 100: "#FFF3C4", + 200: "#FCE588", + 300: "#FADB5F", + 400: "#F7C948", + 500: "#F0B429", + 600: "#DE911D", + 700: "#CB6E17", + 800: "#B44D12", + 900: "#8D2B0B", + }, + + "primary-blue": { + 50: "#E3F8FF", + 100: "#B3ECFF", + 200: "#81DEFD", + 300: "#5ED0FA", + 400: "#40C3F7", + 500: "#2BB0ED", + 600: "#1992D4", + 700: "#127FBF", + 800: "#0B69A3", + 900: "#035388", + }, + + + + "support-red": { + 50: "#FFE3E3", + 100: "#FFBDBD", + 200: "#FF9B9B", + 300: "#F86A6A", + 400: "#EF4E4E", + 500: "#E12D39", + 600: "#CF1124", + 700: "#AB091E", + 800: "#8A041A", + 900: "#610316", + }, + + "support-teal": { + 50: "#EFFCF6", + 100: "#C6F7E2", + 200: "#8EEDC7", + 300: "#65D6AD", + 400: "#3EBD93", + 500: "#27AB83", + 600: "#199473", + 700: "#147D64", + 800: "#0C6B58", + 900: "#014D40", + }, + }, + }, + }, + plugins: [], +}; diff --git a/front/tsconfig.json b/front/tsconfig.json new file mode 100644 index 0000000..a7fc6fb --- /dev/null +++ b/front/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/front/tsconfig.node.json b/front/tsconfig.node.json new file mode 100644 index 0000000..97ede7e --- /dev/null +++ b/front/tsconfig.node.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "strict": true + }, + "include": ["vite.config.ts"] +} diff --git a/front/vite.config.ts b/front/vite.config.ts new file mode 100644 index 0000000..318b03b --- /dev/null +++ b/front/vite.config.ts @@ -0,0 +1,21 @@ +import react from "@vitejs/plugin-react"; +import { defineConfig } from "vite"; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], + resolve: { + alias: { + "@": "/src", + }, + }, + server: { + proxy: { + "/api": { + target: "http://localhost:8080", + secure: false, + changeOrigin: true, + }, + }, + }, +}); diff --git a/go.mod b/go.mod index 69e7dfe..88aabfd 100644 --- a/go.mod +++ b/go.mod @@ -3,53 +3,83 @@ module git.lehouerou.net/laurent/sorarebuddy go 1.22.1 require ( + github.com/hashicorp/go-retryablehttp v0.7.5 github.com/jackc/pgx/v5 v5.5.5 - github.com/llehouerou/go-graphql-client v0.9.6 - github.com/shopspring/decimal v1.3.1 + github.com/llehouerou/go-graphql-client v0.9.7 + github.com/shopspring/decimal v1.4.0 + github.com/sourcegraph/conc v0.3.0 github.com/spf13/cobra v1.8.0 github.com/spf13/viper v1.18.2 + github.com/uptrace/bun v1.1.17 + github.com/uptrace/bun/dialect/pgdialect v1.1.17 ) require ( + github.com/andybalholm/cascadia v1.3.2 // indirect + github.com/antchfx/htmlquery v1.3.1 // indirect + github.com/antchfx/xmlquery v1.4.0 // indirect + github.com/antchfx/xpath v1.3.0 // indirect + github.com/fatih/color v1.16.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/gobwas/glob v0.2.3 // indirect + github.com/golang-jwt/jwt v3.2.2+incompatible // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.3 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/websocket v1.5.1 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect - github.com/hashicorp/go-retryablehttp v0.7.5 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/klauspost/compress v1.17.0 // indirect + github.com/jackc/puddle/v2 v2.2.1 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/kennygrant/sanitize v1.2.4 // indirect + github.com/labstack/gommon v0.4.2 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.19 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/pelletier/go-toml/v2 v2.1.0 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect - github.com/sourcegraph/conc v0.3.0 // indirect + github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.6.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/subosito/gotenv v1.6.0 // indirect + github.com/temoto/robotstxt v1.1.2 // indirect + github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasttemplate v1.2.2 // indirect + github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect + github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/xlzd/gotp v0.1.0 // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect - golang.org/x/net v0.21.0 // indirect - golang.org/x/sys v0.18.0 // indirect + golang.org/x/net v0.24.0 // indirect + golang.org/x/sync v0.5.0 // indirect + golang.org/x/sys v0.19.0 // indirect golang.org/x/time v0.5.0 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/protobuf v1.31.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - nhooyr.io/websocket v1.8.7 // indirect + nhooyr.io/websocket v1.8.11 // indirect ) require ( - git.lehouerou.net/laurent/sorare v0.0.0-20240308104821-b99d85580526 + git.lehouerou.net/laurent/sorare v0.1.9 + github.com/PuerkitoBio/goquery v1.9.1 + github.com/dustin/go-humanize v1.0.1 + github.com/gocolly/colly v1.2.0 github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect + github.com/labstack/echo/v4 v4.11.4 github.com/pkg/errors v0.9.1 + github.com/robfig/cron/v3 v3.0.1 github.com/rs/zerolog v1.32.0 github.com/samber/lo v1.39.0 - golang.org/x/crypto v0.21.0 // indirect + github.com/uptrace/bun/extra/bundebug v1.1.17 + golang.org/x/crypto v0.22.0 // indirect golang.org/x/text v0.14.0 // indirect ) diff --git a/go.sum b/go.sum index 70cf21a..17ff196 100644 --- a/go.sum +++ b/go.sum @@ -1,46 +1,47 @@ -git.lehouerou.net/laurent/sorare v0.0.0-20240308104821-b99d85580526 h1:/qgeHqVliEYKynyyDEnS45e6I8uS7bAFkw1ucpQ3D8I= -git.lehouerou.net/laurent/sorare v0.0.0-20240308104821-b99d85580526/go.mod h1:s/kVH8wu+esjAd1Zw+8+UGovg5hVClZp+9lFu7ZXsbI= +git.lehouerou.net/laurent/sorare v0.1.9 h1:puSzj3KcX+BYZcIsR5DO1iZJEC8YR7i+aE09+VZoEA8= +git.lehouerou.net/laurent/sorare v0.1.9/go.mod h1:Si2h5uCc4ELc0qEZFgiYLhJIm9B9NGJAAkaYYTAE978= +github.com/PuerkitoBio/goquery v1.9.1 h1:mTL6XjbJTZdpfL+Gwl5U2h1l9yEkJjhmlTeV9VPW7UI= +github.com/PuerkitoBio/goquery v1.9.1/go.mod h1:cW1n6TmIMDoORQU5IU/P1T3tGFunOeXEpGP2WHRwkbY= +github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss= +github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU= +github.com/antchfx/htmlquery v1.3.1 h1:wm0LxjLMsZhRHfQKKZscDf2COyH4vDYA3wyH+qZ+Ylc= +github.com/antchfx/htmlquery v1.3.1/go.mod h1:PTj+f1V2zksPlwNt7uVvZPsxpKNa7mlVliCRxLX6Nx8= +github.com/antchfx/xmlquery v1.4.0 h1:xg2HkfcRK2TeTbdb0m1jxCYnvsPaGY/oeZWTGqX/0hA= +github.com/antchfx/xmlquery v1.4.0/go.mod h1:Ax2aeaeDjfIw3CwXKDQ0GkwZ6QlxoChlIBP+mGnDFjI= +github.com/antchfx/xpath v1.3.0 h1:nTMlzGAK3IJ0bPpME2urTuFL76o4A96iYvoKFHRXJgc= +github.com/antchfx/xpath v1.3.0/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w= -github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= -github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= -github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= -github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= -github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= -github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= -github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= -github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= -github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= -github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= -github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= -github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= -github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= -github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8= -github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= -github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo= -github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/gocolly/colly v1.2.0 h1:qRz9YAn8FIH0qzgNUw+HT9UN7wm1oF9OBAilwEWpyrI= +github.com/gocolly/colly v1.2.0/go.mod h1:Hof5T3ZswNVsOHYmba1u03W65HDWgpV5HifSuueE0EA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 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/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= github.com/graph-gophers/graphql-go v1.5.0 h1:fDqblo50TEpD0LY7RXk/LFVYEVqo3+tXMNMPSVXA1yc= @@ -66,36 +67,30 @@ github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw= github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= -github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= -github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM= -github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/kennygrant/sanitize v1.2.4 h1:gN25/otpP5vAsO2djbMhF/LQX6R7+O1TB4yv8NzpJ3o= +github.com/kennygrant/sanitize v1.2.4/go.mod h1:LGsjYYtgxbetdg5owWB2mpgUL6e2nfw2eObZ0u0qvak= 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/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= -github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= -github.com/llehouerou/go-graphql-client v0.9.6 h1:tZdCHrBz8p044KoxXwmEur9L4VNZ3luYeJmQZpLodPY= -github.com/llehouerou/go-graphql-client v0.9.6/go.mod h1:hyvnfe3diGLxMelSKF+cFPyAfxZLa1CGGBogmix4SyM= +github.com/labstack/echo/v4 v4.11.4 h1:vDZmA+qNeh1pd/cCkEicDMrjtrnMGQ1QFI9gWN1zGq8= +github.com/labstack/echo/v4 v4.11.4/go.mod h1:noh7EvLwqDsmh/X/HWKPUl1AjzJrhyptRyEbQJfxen8= +github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= +github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= +github.com/llehouerou/go-graphql-client v0.9.7 h1:cDDy69jNbVhcfA6Bmmk0tYeaLArsrIA/lohAQxA5D3Q= +github.com/llehouerou/go-graphql-client v0.9.7/go.mod h1:hyvnfe3diGLxMelSKF+cFPyAfxZLa1CGGBogmix4SyM= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -103,6 +98,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/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.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= @@ -113,10 +110,12 @@ github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6ke github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7AwssoOcM/tq5JjjG2yYOc8odClEiXA= +github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU= github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA= github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= -github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= -github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= @@ -134,7 +133,6 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -142,38 +140,89 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= -github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= -github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= -github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/temoto/robotstxt v1.1.2 h1:W2pOjSJ6SWvldyEuiFXNxz3xZ8aiWX5LbfDiOFd7Fxg= +github.com/temoto/robotstxt v1.1.2/go.mod h1:+1AmkuG3IYkh1kv0d2qEB9Le88ehNO0zwOr3ujewlOo= +github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo= +github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs= +github.com/uptrace/bun v1.1.17 h1:qxBaEIo0hC/8O3O6GrMDKxqyT+mw5/s0Pn/n6xjyGIk= +github.com/uptrace/bun v1.1.17/go.mod h1:hATAzivtTIRsSJR4B8AXR+uABqnQxr3myKDKEf5iQ9U= +github.com/uptrace/bun/dialect/pgdialect v1.1.17 h1:NsvFVHAx1Az6ytlAD/B6ty3cVE6j9Yp82bjqd9R9hOs= +github.com/uptrace/bun/dialect/pgdialect v1.1.17/go.mod h1:fLBDclNc7nKsZLzNjFL6BqSdgJzbj2HdnyOnLoDvAME= +github.com/uptrace/bun/extra/bundebug v1.1.17 h1:LcZ8DzyyGdXAmbUqmnCpBq7TPFegMp59FGy+uzEE21c= +github.com/uptrace/bun/extra/bundebug v1.1.17/go.mod h1:FOwNaBEGGChv3qBVh3pz3TPlUuikZ93qKjd/LJdl91o= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= +github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= +github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/xlzd/gotp v0.1.0 h1:37blvlKCh38s+fkem+fFh7sMnceltoIEBYTVXyoa5Po= github.com/xlzd/gotp v0.1.0/go.mod h1:ndLJ3JKzi3xLmUProq4LLxCuECL93dG9WASNLpHz8qg= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= -golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= -golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= -golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= -golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= +golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -181,11 +230,8 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g= -nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= +nhooyr.io/websocket v1.8.11 h1:f/qXNc2/3DpoSZkHt1DQu6rj4zGC8JmkkLkWss0MgN0= +nhooyr.io/websocket v1.8.11/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c= diff --git a/model/batch.go b/model/batch.go deleted file mode 100644 index 049b37f..0000000 --- a/model/batch.go +++ /dev/null @@ -1,809 +0,0 @@ -// Code generated by sqlc. DO NOT EDIT. -// versions: -// sqlc v1.25.0 -// source: batch.go - -package model - -import ( - "context" - "errors" - "time" - - "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/pgtype" - "github.com/shopspring/decimal" -) - -var ( - ErrBatchAlreadyClosed = errors.New("batch already closed") -) - -const createOrUpdateCompetitions = `-- name: CreateOrUpdateCompetitions :batchexec - INSERT INTO competitions ( - slug, - display_name, - country_slug, - competition_format, - competition_type, - picture_url, - logo_url - ) - VALUES ($1, $2, $3, $4, $5, $6, $7) ON CONFLICT (slug) - DO - UPDATE - SET display_name = EXCLUDED.display_name, - competition_format = EXCLUDED.competition_format, - competition_type = EXCLUDED.competition_type, - picture_url = EXCLUDED.picture_url, - logo_url = EXCLUDED.logo_url, - country_slug = EXCLUDED.country_slug -` - -type CreateOrUpdateCompetitionsBatchResults struct { - br pgx.BatchResults - tot int - closed bool -} - -type CreateOrUpdateCompetitionsParams struct { - Slug string - DisplayName string - CountrySlug string - CompetitionFormat string - CompetitionType string - PictureUrl string - LogoUrl string -} - -func (q *Queries) CreateOrUpdateCompetitions(ctx context.Context, arg []CreateOrUpdateCompetitionsParams) *CreateOrUpdateCompetitionsBatchResults { - batch := &pgx.Batch{} - for _, a := range arg { - vals := []interface{}{ - a.Slug, - a.DisplayName, - a.CountrySlug, - a.CompetitionFormat, - a.CompetitionType, - a.PictureUrl, - a.LogoUrl, - } - batch.Queue(createOrUpdateCompetitions, vals...) - } - br := q.db.SendBatch(ctx, batch) - return &CreateOrUpdateCompetitionsBatchResults{br, len(arg), false} -} - -func (b *CreateOrUpdateCompetitionsBatchResults) Exec(f func(int, error)) { - defer b.br.Close() - for t := 0; t < b.tot; t++ { - if b.closed { - if f != nil { - f(t, ErrBatchAlreadyClosed) - } - continue - } - _, err := b.br.Exec() - if f != nil { - f(t, err) - } - } -} - -func (b *CreateOrUpdateCompetitionsBatchResults) Close() error { - b.closed = true - return b.br.Close() -} - -const createOrUpdateCountries = `-- name: CreateOrUpdateCountries :batchexec -INSERT INTO countries ( - slug, - code, - display_name, - three_letter_code, - flag_flat_64_url, - flag_flat_32_url, - flag_round_64_url, - flag_round_32_url - ) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8) - ON CONFLICT (slug) - DO UPDATE - SET code = EXCLUDED.code, - display_name = EXCLUDED.display_name, - three_letter_code = EXCLUDED.three_letter_code, - flag_flat_64_url = EXCLUDED.flag_flat_64_url, - flag_flat_32_url = EXCLUDED.flag_flat_32_url, - flag_round_64_url = EXCLUDED.flag_round_64_url, - flag_round_32_url = EXCLUDED.flag_round_32_url -` - -type CreateOrUpdateCountriesBatchResults struct { - br pgx.BatchResults - tot int - closed bool -} - -type CreateOrUpdateCountriesParams struct { - Slug string - Code string - DisplayName string - ThreeLetterCode string - FlagFlat64Url string - FlagFlat32Url string - FlagRound64Url string - FlagRound32Url string -} - -func (q *Queries) CreateOrUpdateCountries(ctx context.Context, arg []CreateOrUpdateCountriesParams) *CreateOrUpdateCountriesBatchResults { - batch := &pgx.Batch{} - for _, a := range arg { - vals := []interface{}{ - a.Slug, - a.Code, - a.DisplayName, - a.ThreeLetterCode, - a.FlagFlat64Url, - a.FlagFlat32Url, - a.FlagRound64Url, - a.FlagRound32Url, - } - batch.Queue(createOrUpdateCountries, vals...) - } - br := q.db.SendBatch(ctx, batch) - return &CreateOrUpdateCountriesBatchResults{br, len(arg), false} -} - -func (b *CreateOrUpdateCountriesBatchResults) Exec(f func(int, error)) { - defer b.br.Close() - for t := 0; t < b.tot; t++ { - if b.closed { - if f != nil { - f(t, ErrBatchAlreadyClosed) - } - continue - } - _, err := b.br.Exec() - if f != nil { - f(t, err) - } - } -} - -func (b *CreateOrUpdateCountriesBatchResults) Close() error { - b.closed = true - return b.br.Close() -} - -const createOrUpdateFixtures = `-- name: CreateOrUpdateFixtures :batchexec -INSERT INTO fixtures (slug, display_name, state, start_date, end_date, game_week) -VALUES ($1, $2, $3, $4, $5, $6) -ON CONFLICT (slug) DO UPDATE -SET display_name = $2, state = $3, start_date = $4, end_date = $5, game_week = $6 -` - -type CreateOrUpdateFixturesBatchResults struct { - br pgx.BatchResults - tot int - closed bool -} - -type CreateOrUpdateFixturesParams struct { - Slug string - DisplayName string - State string - StartDate pgtype.Timestamptz - EndDate pgtype.Timestamptz - GameWeek int32 -} - -func (q *Queries) CreateOrUpdateFixtures(ctx context.Context, arg []CreateOrUpdateFixturesParams) *CreateOrUpdateFixturesBatchResults { - batch := &pgx.Batch{} - for _, a := range arg { - vals := []interface{}{ - a.Slug, - a.DisplayName, - a.State, - a.StartDate, - a.EndDate, - a.GameWeek, - } - batch.Queue(createOrUpdateFixtures, vals...) - } - br := q.db.SendBatch(ctx, batch) - return &CreateOrUpdateFixturesBatchResults{br, len(arg), false} -} - -func (b *CreateOrUpdateFixturesBatchResults) Exec(f func(int, error)) { - defer b.br.Close() - for t := 0; t < b.tot; t++ { - if b.closed { - if f != nil { - f(t, ErrBatchAlreadyClosed) - } - continue - } - _, err := b.br.Exec() - if f != nil { - f(t, err) - } - } -} - -func (b *CreateOrUpdateFixturesBatchResults) Close() error { - b.closed = true - return b.br.Close() -} - -const createOrUpdateGamePlayerScores = `-- name: CreateOrUpdateGamePlayerScores :batchexec -INSERT INTO game_player_scores( - game_id, - player_slug, - score, - decisive_score, - all_around_score, - minutes_played, - game_started, - formation_place, - live, - on_game_sheet, - reviewed, - goal, - assist, - penalty_won, - clearance_off_line, - last_man_tackle, - penalty_save, - own_goal, - red_card, - error_lead_to_goal, - penalty_conceded, - yellow_card, - fouls, - fouled, - clean_sheet, - double_double, - triple_double, - triple_triple, - error_lead_to_shot, - saves, - saved_shot_from_inside_box, - good_high_claim, - punches, - diving_save, - diving_catch, - cross_not_claimed, - goalkeeper_smother, - six_second_violation, - keeper_sweeper, - goals_conceded, - effective_clearance, - won_tackle, - blocked_cross, - block, - possession_lost, - possession_won, - duel_lost, - duel_won, - interception, - accurate_pass, - accurate_final_third_pass, - accurate_long_ball, - long_pass_into_opposition, - missed_pass, - shot_on_target, - won_contest, - big_chance_created, - attempted_assist, - penalty_area_entries, - penalty_kick_missed, - big_chance_missed) -VALUES( - $1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24,$25,$26,$27,$28,$29,$30,$31, - $32,$33,$34,$35,$36,$37,$38,$39,$40,$41,$42,$43,$44,$45,$46,$47,$48,$49,$50,$51,$52,$53,$54,$55,$56,$57,$58,$59,$60, - $61) -ON CONFLICT (game_id, player_slug) DO UPDATE -SET score = EXCLUDED.score, - decisive_score = EXCLUDED.decisive_score, - all_around_score = EXCLUDED.all_around_score, - minutes_played = EXCLUDED.minutes_played, - game_started = EXCLUDED.game_started, - formation_place = EXCLUDED.formation_place, - live = EXCLUDED.live, - on_game_sheet = EXCLUDED.on_game_sheet, - reviewed = EXCLUDED.reviewed, - goal = EXCLUDED.goal, - assist = EXCLUDED.assist, - penalty_won = EXCLUDED.penalty_won, - clearance_off_line = EXCLUDED.clearance_off_line, - last_man_tackle = EXCLUDED.last_man_tackle, - penalty_save = EXCLUDED.penalty_save, - own_goal = EXCLUDED.own_goal, - red_card = EXCLUDED.red_card, - error_lead_to_goal = EXCLUDED.error_lead_to_goal, - penalty_conceded = EXCLUDED.penalty_conceded, - yellow_card = EXCLUDED.yellow_card, - fouls = EXCLUDED.fouls, - fouled = EXCLUDED.fouled, - clean_sheet = EXCLUDED.clean_sheet, - double_double = EXCLUDED.double_double, - triple_double = EXCLUDED.triple_double, - triple_triple = EXCLUDED.triple_triple, - error_lead_to_shot = EXCLUDED.error_lead_to_shot, - saves = EXCLUDED.saves, - saved_shot_from_inside_box = EXCLUDED.saved_shot_from_inside_box, - good_high_claim = EXCLUDED.good_high_claim, - punches = EXCLUDED.punches, - diving_save = EXCLUDED.diving_save, - diving_catch = EXCLUDED.diving_catch, - cross_not_claimed = EXCLUDED.cross_not_claimed, - goalkeeper_smother = EXCLUDED.goalkeeper_smother, - six_second_violation = EXCLUDED.six_second_violation, - keeper_sweeper = EXCLUDED.keeper_sweeper, - goals_conceded = EXCLUDED.goals_conceded, - effective_clearance = EXCLUDED.effective_clearance, - won_tackle = EXCLUDED.won_tackle, - blocked_cross = EXCLUDED.blocked_cross, - block = EXCLUDED.block, - possession_lost = EXCLUDED.possession_lost, - possession_won = EXCLUDED.possession_won, - duel_lost = EXCLUDED.duel_lost, - duel_won = EXCLUDED.duel_won, - interception = EXCLUDED.interception, - accurate_pass = EXCLUDED.accurate_pass, - accurate_final_third_pass = EXCLUDED.accurate_final_third_pass, - accurate_long_ball = EXCLUDED.accurate_long_ball, - long_pass_into_opposition = EXCLUDED.long_pass_into_opposition, - missed_pass = EXCLUDED.missed_pass, - shot_on_target = EXCLUDED.shot_on_target, - won_contest = EXCLUDED.won_contest, - big_chance_created = EXCLUDED.big_chance_created, - attempted_assist = EXCLUDED.attempted_assist, - penalty_area_entries = EXCLUDED.penalty_area_entries, - penalty_kick_missed = EXCLUDED.penalty_kick_missed, - big_chance_missed = EXCLUDED.big_chance_missed -` - -type CreateOrUpdateGamePlayerScoresBatchResults struct { - br pgx.BatchResults - tot int - closed bool -} - -type CreateOrUpdateGamePlayerScoresParams struct { - GameID string - PlayerSlug string - Score decimal.Decimal - DecisiveScore decimal.Decimal - AllAroundScore decimal.Decimal - MinutesPlayed int32 - GameStarted bool - FormationPlace int32 - Live bool - OnGameSheet bool - Reviewed bool - Goal int32 - Assist int32 - PenaltyWon int32 - ClearanceOffLine int32 - LastManTackle int32 - PenaltySave int32 - OwnGoal int32 - RedCard bool - ErrorLeadToGoal int32 - PenaltyConceded int32 - YellowCard int32 - Fouls int32 - Fouled int32 - CleanSheet bool - DoubleDouble bool - TripleDouble bool - TripleTriple bool - ErrorLeadToShot int32 - Saves int32 - SavedShotFromInsideBox int32 - GoodHighClaim int32 - Punches int32 - DivingSave int32 - DivingCatch int32 - CrossNotClaimed int32 - GoalkeeperSmother int32 - SixSecondViolation int32 - KeeperSweeper int32 - GoalsConceded int32 - EffectiveClearance int32 - WonTackle int32 - BlockedCross int32 - Block int32 - PossessionLost int32 - PossessionWon int32 - DuelLost int32 - DuelWon int32 - Interception int32 - AccuratePass int32 - AccurateFinalThirdPass int32 - AccurateLongBall int32 - LongPassIntoOpposition int32 - MissedPass int32 - ShotOnTarget int32 - WonContest int32 - BigChanceCreated int32 - AttemptedAssist int32 - PenaltyAreaEntries int32 - PenaltyKickMissed int32 - BigChanceMissed int32 -} - -func (q *Queries) CreateOrUpdateGamePlayerScores(ctx context.Context, arg []CreateOrUpdateGamePlayerScoresParams) *CreateOrUpdateGamePlayerScoresBatchResults { - batch := &pgx.Batch{} - for _, a := range arg { - vals := []interface{}{ - a.GameID, - a.PlayerSlug, - a.Score, - a.DecisiveScore, - a.AllAroundScore, - a.MinutesPlayed, - a.GameStarted, - a.FormationPlace, - a.Live, - a.OnGameSheet, - a.Reviewed, - a.Goal, - a.Assist, - a.PenaltyWon, - a.ClearanceOffLine, - a.LastManTackle, - a.PenaltySave, - a.OwnGoal, - a.RedCard, - a.ErrorLeadToGoal, - a.PenaltyConceded, - a.YellowCard, - a.Fouls, - a.Fouled, - a.CleanSheet, - a.DoubleDouble, - a.TripleDouble, - a.TripleTriple, - a.ErrorLeadToShot, - a.Saves, - a.SavedShotFromInsideBox, - a.GoodHighClaim, - a.Punches, - a.DivingSave, - a.DivingCatch, - a.CrossNotClaimed, - a.GoalkeeperSmother, - a.SixSecondViolation, - a.KeeperSweeper, - a.GoalsConceded, - a.EffectiveClearance, - a.WonTackle, - a.BlockedCross, - a.Block, - a.PossessionLost, - a.PossessionWon, - a.DuelLost, - a.DuelWon, - a.Interception, - a.AccuratePass, - a.AccurateFinalThirdPass, - a.AccurateLongBall, - a.LongPassIntoOpposition, - a.MissedPass, - a.ShotOnTarget, - a.WonContest, - a.BigChanceCreated, - a.AttemptedAssist, - a.PenaltyAreaEntries, - a.PenaltyKickMissed, - a.BigChanceMissed, - } - batch.Queue(createOrUpdateGamePlayerScores, vals...) - } - br := q.db.SendBatch(ctx, batch) - return &CreateOrUpdateGamePlayerScoresBatchResults{br, len(arg), false} -} - -func (b *CreateOrUpdateGamePlayerScoresBatchResults) Exec(f func(int, error)) { - defer b.br.Close() - for t := 0; t < b.tot; t++ { - if b.closed { - if f != nil { - f(t, ErrBatchAlreadyClosed) - } - continue - } - _, err := b.br.Exec() - if f != nil { - f(t, err) - } - } -} - -func (b *CreateOrUpdateGamePlayerScoresBatchResults) Close() error { - b.closed = true - return b.br.Close() -} - -const createOrUpdateGamePlayers = `-- name: CreateOrUpdateGamePlayers :batchexec -INSERT INTO game_players( - game_id, - player_slug, - status, - team_slug) -VALUES( - $1, - $2, - $3, - $4) - ON CONFLICT (game_id, player_slug) DO UPDATE - SET status = $3, - team_slug = $4 -` - -type CreateOrUpdateGamePlayersBatchResults struct { - br pgx.BatchResults - tot int - closed bool -} - -type CreateOrUpdateGamePlayersParams struct { - GameID string - PlayerSlug string - Status string - TeamSlug string -} - -func (q *Queries) CreateOrUpdateGamePlayers(ctx context.Context, arg []CreateOrUpdateGamePlayersParams) *CreateOrUpdateGamePlayersBatchResults { - batch := &pgx.Batch{} - for _, a := range arg { - vals := []interface{}{ - a.GameID, - a.PlayerSlug, - a.Status, - a.TeamSlug, - } - batch.Queue(createOrUpdateGamePlayers, vals...) - } - br := q.db.SendBatch(ctx, batch) - return &CreateOrUpdateGamePlayersBatchResults{br, len(arg), false} -} - -func (b *CreateOrUpdateGamePlayersBatchResults) Exec(f func(int, error)) { - defer b.br.Close() - for t := 0; t < b.tot; t++ { - if b.closed { - if f != nil { - f(t, ErrBatchAlreadyClosed) - } - continue - } - _, err := b.br.Exec() - if f != nil { - f(t, err) - } - } -} - -func (b *CreateOrUpdateGamePlayersBatchResults) Close() error { - b.closed = true - return b.br.Close() -} - -const createOrUpdateGames = `-- name: CreateOrUpdateGames :batchexec -INSERT INTO games (id, date, coverage_status, low_coverage, minutes, period_type, scored, status, competition_slug, fixture_slug, away_team_slug, away_goals, away_extra_time_score, away_penalty_score, home_team_slug, home_goals, home_extra_time_score, home_penalty_score, winner_team_slug) -VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19) -ON CONFLICT (id) DO UPDATE -SET date = EXCLUDED.date, coverage_status = EXCLUDED.coverage_status, low_coverage = EXCLUDED.low_coverage, minutes = EXCLUDED.minutes, period_type = EXCLUDED.period_type, scored = EXCLUDED.scored, status = EXCLUDED.status, competition_slug = EXCLUDED.competition_slug, fixture_slug = EXCLUDED.fixture_slug, away_team_slug = EXCLUDED.away_team_slug, away_goals = EXCLUDED.away_goals, away_extra_time_score = EXCLUDED.away_extra_time_score, away_penalty_score = EXCLUDED.away_penalty_score, home_team_slug = EXCLUDED.home_team_slug, home_goals = EXCLUDED.home_goals, home_extra_time_score = EXCLUDED.home_extra_time_score, home_penalty_score = EXCLUDED.home_penalty_score, winner_team_slug = EXCLUDED.winner_team_slug -` - -type CreateOrUpdateGamesBatchResults struct { - br pgx.BatchResults - tot int - closed bool -} - -type CreateOrUpdateGamesParams struct { - ID string - Date pgtype.Timestamptz - CoverageStatus string - LowCoverage bool - Minutes int32 - PeriodType string - Scored bool - Status string - CompetitionSlug string - FixtureSlug string - AwayTeamSlug string - AwayGoals int32 - AwayExtraTimeScore int32 - AwayPenaltyScore int32 - HomeTeamSlug string - HomeGoals int32 - HomeExtraTimeScore int32 - HomePenaltyScore int32 - WinnerTeamSlug *string -} - -func (q *Queries) CreateOrUpdateGames(ctx context.Context, arg []CreateOrUpdateGamesParams) *CreateOrUpdateGamesBatchResults { - batch := &pgx.Batch{} - for _, a := range arg { - vals := []interface{}{ - a.ID, - a.Date, - a.CoverageStatus, - a.LowCoverage, - a.Minutes, - a.PeriodType, - a.Scored, - a.Status, - a.CompetitionSlug, - a.FixtureSlug, - a.AwayTeamSlug, - a.AwayGoals, - a.AwayExtraTimeScore, - a.AwayPenaltyScore, - a.HomeTeamSlug, - a.HomeGoals, - a.HomeExtraTimeScore, - a.HomePenaltyScore, - a.WinnerTeamSlug, - } - batch.Queue(createOrUpdateGames, vals...) - } - br := q.db.SendBatch(ctx, batch) - return &CreateOrUpdateGamesBatchResults{br, len(arg), false} -} - -func (b *CreateOrUpdateGamesBatchResults) Exec(f func(int, error)) { - defer b.br.Close() - for t := 0; t < b.tot; t++ { - if b.closed { - if f != nil { - f(t, ErrBatchAlreadyClosed) - } - continue - } - _, err := b.br.Exec() - if f != nil { - f(t, err) - } - } -} - -func (b *CreateOrUpdateGamesBatchResults) Close() error { - b.closed = true - return b.br.Close() -} - -const createOrUpdatePlayers = `-- name: CreateOrUpdatePlayers :batchexec -INSERT INTO players (slug, display_name, birth_date, country_slug, team_slug, domestic_league_slug, avatar_url, field_position, status, shirt_number) -VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) -ON CONFLICT (slug) DO UPDATE -SET display_name = $2, birth_date = $3, country_slug = $4, team_slug = $5, domestic_league_slug = $6, avatar_url = $7, field_position = $8, status = $9, shirt_number = $10 -` - -type CreateOrUpdatePlayersBatchResults struct { - br pgx.BatchResults - tot int - closed bool -} - -type CreateOrUpdatePlayersParams struct { - Slug string - DisplayName string - BirthDate time.Time - CountrySlug string - TeamSlug *string - DomesticLeagueSlug *string - AvatarUrl string - FieldPosition string - Status string - ShirtNumber int32 -} - -func (q *Queries) CreateOrUpdatePlayers(ctx context.Context, arg []CreateOrUpdatePlayersParams) *CreateOrUpdatePlayersBatchResults { - batch := &pgx.Batch{} - for _, a := range arg { - vals := []interface{}{ - a.Slug, - a.DisplayName, - a.BirthDate, - a.CountrySlug, - a.TeamSlug, - a.DomesticLeagueSlug, - a.AvatarUrl, - a.FieldPosition, - a.Status, - a.ShirtNumber, - } - batch.Queue(createOrUpdatePlayers, vals...) - } - br := q.db.SendBatch(ctx, batch) - return &CreateOrUpdatePlayersBatchResults{br, len(arg), false} -} - -func (b *CreateOrUpdatePlayersBatchResults) Exec(f func(int, error)) { - defer b.br.Close() - for t := 0; t < b.tot; t++ { - if b.closed { - if f != nil { - f(t, ErrBatchAlreadyClosed) - } - continue - } - _, err := b.br.Exec() - if f != nil { - f(t, err) - } - } -} - -func (b *CreateOrUpdatePlayersBatchResults) Close() error { - b.closed = true - return b.br.Close() -} - -const createOrUpdateTeams = `-- name: CreateOrUpdateTeams :batchexec -INSERT INTO teams (slug, display_name, country_slug, domestic_league_slug, short_name, picture_url, team_type) -VALUES ($1, $2, $3, $4, $5, $6, $7) -ON CONFLICT (slug) DO UPDATE -SET display_name = $2, country_slug = $3, domestic_league_slug = $4, short_name = $5, picture_url = $6, team_type = $7 -` - -type CreateOrUpdateTeamsBatchResults struct { - br pgx.BatchResults - tot int - closed bool -} - -type CreateOrUpdateTeamsParams struct { - Slug string - DisplayName string - CountrySlug string - DomesticLeagueSlug *string - ShortName string - PictureUrl string - TeamType string -} - -func (q *Queries) CreateOrUpdateTeams(ctx context.Context, arg []CreateOrUpdateTeamsParams) *CreateOrUpdateTeamsBatchResults { - batch := &pgx.Batch{} - for _, a := range arg { - vals := []interface{}{ - a.Slug, - a.DisplayName, - a.CountrySlug, - a.DomesticLeagueSlug, - a.ShortName, - a.PictureUrl, - a.TeamType, - } - batch.Queue(createOrUpdateTeams, vals...) - } - br := q.db.SendBatch(ctx, batch) - return &CreateOrUpdateTeamsBatchResults{br, len(arg), false} -} - -func (b *CreateOrUpdateTeamsBatchResults) Exec(f func(int, error)) { - defer b.br.Close() - for t := 0; t < b.tot; t++ { - if b.closed { - if f != nil { - f(t, ErrBatchAlreadyClosed) - } - continue - } - _, err := b.br.Exec() - if f != nil { - f(t, err) - } - } -} - -func (b *CreateOrUpdateTeamsBatchResults) Close() error { - b.closed = true - return b.br.Close() -} diff --git a/model/card.go b/model/card.go new file mode 100644 index 0000000..3bde92b --- /dev/null +++ b/model/card.go @@ -0,0 +1,65 @@ +package model + +import ( + "git.lehouerou.net/laurent/sorare/football" + "git.lehouerou.net/laurent/sorare/types" + "github.com/shopspring/decimal" +) + +type Card struct { + Id string `json:"id"` + AssetId string `json:"assetId"` + PlayerSlug string `json:"playerSlug"` + PlayerDisplayName string `json:"playerDisplayName"` + Rarity types.Rarity `json:"rarity"` + SerialNumber int `json:"serialNumber"` + SeasonStartYear int `json:"seasonStartYear"` + SingleCivilYear bool `json:"singleCivilYear"` + Supply int `json:"supply"` + TeamSlug string `json:"teamSlug"` + Name string `json:"name"` + PictureUrl string `json:"pictureUrl"` + Slug string `json:"slug"` + Power decimal.Decimal `json:"power"` + PowerMalusAfterTransfer decimal.Decimal `json:"powerMalusAfterTransfer"` + RivalsPower decimal.Decimal `json:"rivalsPower"` + Grade int `json:"grade"` + GradeAfterTransfer int `json:"gradeAfterTransfer"` + Xp int `json:"xp"` + XpAfterTransfer int `json:"xpAfterTransfer"` + XpNeededForNextGrade int `json:"xpNeededForNextGrade"` + XpNeededForCurrentGrade int `json:"xpNeededForCurrentGrade"` + InSeasonEligible bool `json:"inSeasonEligible"` + LevelUpAppliedCount int `json:"levelUpAppliedCount"` + MaxLevelUpAppliedCount int `json:"maxLevelUpAppliedCount"` +} + +func NewCardFromSorare(card football.Card) Card { + return Card{ + Id: card.Token.Id.Value, + AssetId: card.Token.AssetId, + PlayerSlug: card.Token.Metadata.Football.PlayerSlug, + PlayerDisplayName: card.Token.Metadata.Football.PlayerDisplayName, + Rarity: types.Rarity(card.Token.Metadata.Football.Rarity), + SerialNumber: card.Token.Metadata.Football.SerialNumber, + SeasonStartYear: card.Token.Metadata.Football.SeasonStartYear, + SingleCivilYear: card.Token.Metadata.Football.SingleCivilYear, + Supply: card.Token.Metadata.Football.Supply, + TeamSlug: card.Token.Metadata.Football.TeamSlug, + Name: card.Token.Name, + PictureUrl: card.Token.PictureUrl, + Slug: card.Token.Slug, + Power: card.Power, + PowerMalusAfterTransfer: card.PowerMalusAfterTransfer, + RivalsPower: card.RivalsPower, + Grade: card.Grade, + GradeAfterTransfer: card.GradeAfterTransfer, + Xp: card.Xp, + XpAfterTransfer: card.XpAfterTransfer, + XpNeededForNextGrade: card.XpNeededForNextGrade, + XpNeededForCurrentGrade: card.XpNeededForCurrentGrade, + InSeasonEligible: card.InSeasonEligible, + LevelUpAppliedCount: card.LevelUpAppliedCount, + MaxLevelUpAppliedCount: card.MaxLevelUpAppliedCount, + } +} diff --git a/model/card_supply.go b/model/card_supply.go new file mode 100644 index 0000000..8bd3790 --- /dev/null +++ b/model/card_supply.go @@ -0,0 +1,15 @@ +package model + +import ( + "time" +) + +type CardSupply struct { + PlayerSlug string `bun:"player_slug,pk" json:"playerSlug"` + SeasonStartYear int `bun:"season_start_year,pk" json:"seasonStartYear"` + Limited int `bun:"limited" json:"limited"` + Rare int `bun:"rare" json:"rare"` + SuperRare int `bun:"super_rare" json:"superRare"` + Unique int `bun:"unique" json:"unique"` + LastUpdated time.Time `bun:"last_updated" json:"lastUpdated"` +} diff --git a/model/competition.go b/model/competition.go new file mode 100644 index 0000000..d94df8f --- /dev/null +++ b/model/competition.go @@ -0,0 +1,15 @@ +package model + +type Competition struct { + Slug string `bun:"slug,pk" json:"slug"` + DisplayName string `bun:"display_name" json:"displayName"` + CountrySlug string `bun:"country_slug" json:"countrySlug"` + CompetitionFormat string `bun:"competition_format" json:"competitionFormat"` + CompetitionType string `bun:"competition_type" json:"competitionType"` + PictureUrl string `bun:"picture_url" json:"pictureUrl"` + LogoUrl string `bun:"logo_url" json:"logoUrl"` + ZoneId *int `bun:"zone_id,scanonly" json:"zoneId"` + + Country *Country `bun:"rel:has-one,join:country_slug=slug" json:"country"` + Zone *Zone `bun:"rel:has-one,join:zone_id=id" json:"zone"` +} diff --git a/model/competition.sql.go b/model/competition.sql.go deleted file mode 100644 index a8d3797..0000000 --- a/model/competition.sql.go +++ /dev/null @@ -1,106 +0,0 @@ -// Code generated by sqlc. DO NOT EDIT. -// versions: -// sqlc v1.25.0 -// source: competition.sql - -package model - -import ( - "context" -) - -const getAllCompetitions = `-- name: GetAllCompetitions :many - SELECT competitions.slug, competitions.display_name, competitions.country_slug, competitions.competition_format, competitions.competition_type, competitions.picture_url, competitions.logo_url, competitions.zone_id, - zones.id, zones.display_name, - countries.slug, countries.code, countries.display_name, countries.three_letter_code, countries.flag_flat_64_url, countries.flag_flat_32_url, countries.flag_round_64_url, countries.flag_round_32_url - FROM competitions - LEFT JOIN zones ON competitions.zone_id = zones.id - LEFT JOIN countries ON competitions.country_slug = countries.slug -` - -type GetAllCompetitionsRow struct { - Competition Competition - Zone Zone - Country Country -} - -func (q *Queries) GetAllCompetitions(ctx context.Context) ([]GetAllCompetitionsRow, error) { - rows, err := q.db.Query(ctx, getAllCompetitions) - if err != nil { - return nil, err - } - defer rows.Close() - var items []GetAllCompetitionsRow - for rows.Next() { - var i GetAllCompetitionsRow - if err := rows.Scan( - &i.Competition.Slug, - &i.Competition.DisplayName, - &i.Competition.CountrySlug, - &i.Competition.CompetitionFormat, - &i.Competition.CompetitionType, - &i.Competition.PictureUrl, - &i.Competition.LogoUrl, - &i.Competition.ZoneID, - &i.Zone.ID, - &i.Zone.DisplayName, - &i.Country.Slug, - &i.Country.Code, - &i.Country.DisplayName, - &i.Country.ThreeLetterCode, - &i.Country.FlagFlat64Url, - &i.Country.FlagFlat32Url, - &i.Country.FlagRound64Url, - &i.Country.FlagRound32Url, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const getCompetitionBySlug = `-- name: GetCompetitionBySlug :one - SELECT competitions.slug, competitions.display_name, competitions.country_slug, competitions.competition_format, competitions.competition_type, competitions.picture_url, competitions.logo_url, competitions.zone_id, - zones.id, zones.display_name, - countries.slug, countries.code, countries.display_name, countries.three_letter_code, countries.flag_flat_64_url, countries.flag_flat_32_url, countries.flag_round_64_url, countries.flag_round_32_url - FROM competitions - LEFT JOIN zones ON competitions.zone_id = zones.id - LEFT JOIN countries ON competitions.country_slug = countries.slug - WHERE competitions.slug = $1 -` - -type GetCompetitionBySlugRow struct { - Competition Competition - Zone Zone - Country Country -} - -func (q *Queries) GetCompetitionBySlug(ctx context.Context, slug string) (GetCompetitionBySlugRow, error) { - row := q.db.QueryRow(ctx, getCompetitionBySlug, slug) - var i GetCompetitionBySlugRow - err := row.Scan( - &i.Competition.Slug, - &i.Competition.DisplayName, - &i.Competition.CountrySlug, - &i.Competition.CompetitionFormat, - &i.Competition.CompetitionType, - &i.Competition.PictureUrl, - &i.Competition.LogoUrl, - &i.Competition.ZoneID, - &i.Zone.ID, - &i.Zone.DisplayName, - &i.Country.Slug, - &i.Country.Code, - &i.Country.DisplayName, - &i.Country.ThreeLetterCode, - &i.Country.FlagFlat64Url, - &i.Country.FlagFlat32Url, - &i.Country.FlagRound64Url, - &i.Country.FlagRound32Url, - ) - return i, err -} diff --git a/model/country.go b/model/country.go new file mode 100644 index 0000000..104b0f0 --- /dev/null +++ b/model/country.go @@ -0,0 +1,12 @@ +package model + +type Country struct { + Slug string `bun:"slug,pk" json:"slug"` + Code string `bun:"code" json:"code"` + DisplayName string `bun:"display_name" json:"displayName"` + ThreeLetterCode string `bun:"three_letter_code" json:"threeLetterCode"` + FlagFlat64Url string `bun:"flag_flat_64_url" json:"flagFlat64Url"` + FlagFlat32Url string `bun:"flag_flat_32_url" json:"flagFlat32Url"` + FlagRound64Url string `bun:"flag_round_64_url" json:"flagRound64Url"` + FlagRound32Url string `bun:"flag_round_32_url" json:"flagRound32Url"` +} diff --git a/model/country.sql.go b/model/country.sql.go deleted file mode 100644 index c4ebf14..0000000 --- a/model/country.sql.go +++ /dev/null @@ -1,80 +0,0 @@ -// Code generated by sqlc. DO NOT EDIT. -// versions: -// sqlc v1.25.0 -// source: country.sql - -package model - -import ( - "context" -) - -const createOrUpdateCountry = `-- name: CreateOrUpdateCountry :exec - INSERT INTO countries ( - slug, - code, - display_name, - three_letter_code, - flag_flat_64_url, - flag_flat_32_url, - flag_round_64_url, - flag_round_32_url - ) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8) ON CONFLICT (slug) - DO - UPDATE - SET code = EXCLUDED.code, - display_name = EXCLUDED.display_name, - three_letter_code = EXCLUDED.three_letter_code, - flag_flat_64_url = EXCLUDED.flag_flat_64_url, - flag_flat_32_url = EXCLUDED.flag_flat_32_url, - flag_round_64_url = EXCLUDED.flag_round_64_url, - flag_round_32_url = EXCLUDED.flag_round_32_url -` - -type CreateOrUpdateCountryParams struct { - Slug string - Code string - DisplayName string - ThreeLetterCode string - FlagFlat64Url string - FlagFlat32Url string - FlagRound64Url string - FlagRound32Url string -} - -func (q *Queries) CreateOrUpdateCountry(ctx context.Context, arg CreateOrUpdateCountryParams) error { - _, err := q.db.Exec(ctx, createOrUpdateCountry, - arg.Slug, - arg.Code, - arg.DisplayName, - arg.ThreeLetterCode, - arg.FlagFlat64Url, - arg.FlagFlat32Url, - arg.FlagRound64Url, - arg.FlagRound32Url, - ) - return err -} - -const getCountryBySlug = `-- name: GetCountryBySlug :one - SELECT slug, code, display_name, three_letter_code, flag_flat_64_url, flag_flat_32_url, flag_round_64_url, flag_round_32_url - FROM countries - WHERE slug = $1 -` - -func (q *Queries) GetCountryBySlug(ctx context.Context, slug string) (Country, error) { - row := q.db.QueryRow(ctx, getCountryBySlug, slug) - var i Country - err := row.Scan( - &i.Slug, - &i.Code, - &i.DisplayName, - &i.ThreeLetterCode, - &i.FlagFlat64Url, - &i.FlagFlat32Url, - &i.FlagRound64Url, - &i.FlagRound32Url, - ) - return i, err -} diff --git a/model/db.go b/model/db.go deleted file mode 100644 index 9d4b732..0000000 --- a/model/db.go +++ /dev/null @@ -1,33 +0,0 @@ -// Code generated by sqlc. DO NOT EDIT. -// versions: -// sqlc v1.25.0 - -package model - -import ( - "context" - - "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/pgconn" -) - -type DBTX interface { - Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error) - Query(context.Context, string, ...interface{}) (pgx.Rows, error) - QueryRow(context.Context, string, ...interface{}) pgx.Row - SendBatch(context.Context, *pgx.Batch) pgx.BatchResults -} - -func New(db DBTX) *Queries { - return &Queries{db: db} -} - -type Queries struct { - db DBTX -} - -func (q *Queries) WithTx(tx pgx.Tx) *Queries { - return &Queries{ - db: tx, - } -} diff --git a/model/fixture.go b/model/fixture.go new file mode 100644 index 0000000..5a7c16b --- /dev/null +++ b/model/fixture.go @@ -0,0 +1,25 @@ +package model + +import ( + "fmt" + "time" +) + +type Fixture struct { + Slug string `bun:"slug,pk" json:"slug"` + DisplayName string `bun:"display_name" json:"displayName"` + State string `bun:"state" json:"fixtureState"` + StartDate time.Time `bun:"start_date" json:"startDate"` + EndDate time.Time `bun:"end_date" json:"endDate"` + GameWeek int `bun:"game_week" json:"gameWeek"` +} + +func (f *Fixture) String() string { + return fmt.Sprintf( + "%s | %7s | %s -> %s", + f.DisplayName, + f.State, + f.StartDate.Format("2006-01-02"), + f.EndDate.Format("2006-01-02"), + ) +} diff --git a/model/fixture.sql.go b/model/fixture.sql.go deleted file mode 100644 index b66dd59..0000000 --- a/model/fixture.sql.go +++ /dev/null @@ -1,77 +0,0 @@ -// Code generated by sqlc. DO NOT EDIT. -// versions: -// sqlc v1.25.0 -// source: fixture.sql - -package model - -import ( - "context" -) - -const getAllFixtures = `-- name: GetAllFixtures :many -SELECT slug, display_name, state, start_date, end_date, game_week FROM fixtures -` - -func (q *Queries) GetAllFixtures(ctx context.Context) ([]Fixture, error) { - rows, err := q.db.Query(ctx, getAllFixtures) - if err != nil { - return nil, err - } - defer rows.Close() - var items []Fixture - for rows.Next() { - var i Fixture - if err := rows.Scan( - &i.Slug, - &i.DisplayName, - &i.State, - &i.StartDate, - &i.EndDate, - &i.GameWeek, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const getFixtureByGameWeek = `-- name: GetFixtureByGameWeek :one -SELECT slug, display_name, state, start_date, end_date, game_week FROM fixtures WHERE game_week = $1 -` - -func (q *Queries) GetFixtureByGameWeek(ctx context.Context, gameWeek int32) (Fixture, error) { - row := q.db.QueryRow(ctx, getFixtureByGameWeek, gameWeek) - var i Fixture - err := row.Scan( - &i.Slug, - &i.DisplayName, - &i.State, - &i.StartDate, - &i.EndDate, - &i.GameWeek, - ) - return i, err -} - -const getFixtureBySlug = `-- name: GetFixtureBySlug :one -SELECT slug, display_name, state, start_date, end_date, game_week FROM fixtures WHERE slug = $1 -` - -func (q *Queries) GetFixtureBySlug(ctx context.Context, slug string) (Fixture, error) { - row := q.db.QueryRow(ctx, getFixtureBySlug, slug) - var i Fixture - err := row.Scan( - &i.Slug, - &i.DisplayName, - &i.State, - &i.StartDate, - &i.EndDate, - &i.GameWeek, - ) - return i, err -} diff --git a/model/game.go b/model/game.go new file mode 100644 index 0000000..d57cdd7 --- /dev/null +++ b/model/game.go @@ -0,0 +1,40 @@ +package model + +import ( + "fmt" + "time" +) + +type Game struct { + Id string `bun:"id,pk" json:"id"` + Date time.Time `bun:"date" json:"date"` + CoverageStatus string `bun:"coverage_status" json:"coverageStatus"` + LowCoverage bool `bun:"low_coverage" json:"lowCoverage"` + Minutes int `bun:"minutes" json:"minutes"` + PeriodType string `bun:"period_type" json:"periodType"` + Scored bool `bun:"scored" json:"scored"` + Status string `bun:"status" json:"status"` + CompetitionSlug string `bun:"competition_slug" json:"competitionSlug"` + FixtureSlug string `bun:"fixture_slug" json:"fixtureSlug"` + AwayTeamSlug string `bun:"away_team_slug" json:"awayTeamSlug"` + AwayGoals int `bun:"away_goals" json:"awayGoals"` + AwayExtraTimeScore int `bun:"away_extra_time_score" json:"awayExtraTimeScore"` + AwayPenaltyScore int `bun:"away_penalty_score" json:"awayPenaltyScore"` + HomeTeamSlug string `bun:"home_team_slug" json:"homeTeamSlug"` + HomeGoals int `bun:"home_goals" json:"homeGoals"` + HomeExtraTimeScore int `bun:"home_extra_time_score" json:"homeExtraTimeScore"` + HomePenaltyScore int `bun:"home_penalty_score" json:"homePenaltyScore"` + WinnerTeamSlug *string `bun:"winner_team_slug" json:"winnerTeamSlug"` + + HomeTeam *Team `bun:"rel:has-one,join:home_team_slug=slug" json:"homeTeam"` + AwayTeam *Team `bun:"rel:has-one,join:away_team_slug=slug" json:"awayTeam"` + WinnerTeam *Team `bun:"rel:has-one,join:winner_team_slug=slug" json:"winnerTeam"` + Competition *Competition `bun:"rel:has-one,join:competition_slug=slug" json:"competition"` + + GamePlayers []GamePlayer `bun:"rel:has-many,join:id=game_id" json:"gamePlayers"` + Fixture *Fixture `bun:"rel:has-one,join:fixture_slug=slug" json:"fixture"` +} + +func (g *Game) String() string { + return fmt.Sprintf("%s %s <-> %s", g.Date.Format("2006-01-02"), g.HomeTeam.DisplayName, g.AwayTeam.DisplayName) +} diff --git a/model/game.sql.go b/model/game.sql.go deleted file mode 100644 index ff11b99..0000000 --- a/model/game.sql.go +++ /dev/null @@ -1,8 +0,0 @@ -// Code generated by sqlc. DO NOT EDIT. -// versions: -// sqlc v1.25.0 -// source: game.sql - -package model - -import () diff --git a/model/game_player.go b/model/game_player.go new file mode 100644 index 0000000..1c3d986 --- /dev/null +++ b/model/game_player.go @@ -0,0 +1,61 @@ +package model + +import ( + "git.lehouerou.net/laurent/sorare/football" +) + +type GamePlayer struct { + GameId string `bun:"game_id,pk" json:"gameId"` + PlayerSlug string `bun:"player_slug,pk" json:"playerSlug"` + Status string `bun:"status" json:"status"` + TeamSlug string `bun:"team_slug" json:"teamSlug"` + + Game *Game `bun:"rel:has-one,join:game_id=id" json:"game"` + Player *Player `bun:"rel:has-one,join:player_slug=slug" json:"player"` + Score *GamePlayerScore `bun:"rel:has-one,join:game_id=game_id,join:player_slug=player_slug" json:"score"` +} + +func ExtractPlayersFromGameWithFormation( + gameWithFormation football.GameFormation, +) []GamePlayer { + var res []GamePlayer + for _, p := range gameWithFormation.HomeFormation.Bench { + res = append(res, GamePlayer{ + GameId: gameWithFormation.Id.Value, + PlayerSlug: p.Slug, + TeamSlug: gameWithFormation.HomeTeam.Team.Slug, + Status: "bench", + }) + } + for _, p := range gameWithFormation.HomeFormation.StartingLineup { + for _, q := range p { + res = append(res, GamePlayer{ + GameId: gameWithFormation.Id.Value, + PlayerSlug: q.Slug, + TeamSlug: gameWithFormation.HomeTeam.Team.Slug, + Status: "starting", + }) + } + } + for _, p := range gameWithFormation.AwayFormation.Bench { + res = append(res, GamePlayer{ + GameId: gameWithFormation.Id.Value, + PlayerSlug: p.Slug, + TeamSlug: gameWithFormation.AwayTeam.Team.Slug, + Status: "bench", + }) + } + for _, p := range gameWithFormation.AwayFormation.StartingLineup { + for _, q := range p { + res = append(res, GamePlayer{ + GameId: gameWithFormation.Id.Value, + PlayerSlug: q.Slug, + TeamSlug: gameWithFormation.AwayTeam.Team.Slug, + Status: "starting", + }) + } + } + + return res + +} diff --git a/model/game_player.sql.go b/model/game_player.sql.go deleted file mode 100644 index 190db48..0000000 --- a/model/game_player.sql.go +++ /dev/null @@ -1,8 +0,0 @@ -// Code generated by sqlc. DO NOT EDIT. -// versions: -// sqlc v1.25.0 -// source: game_player.sql - -package model - -import () diff --git a/model/game_player_score.go b/model/game_player_score.go new file mode 100644 index 0000000..2587656 --- /dev/null +++ b/model/game_player_score.go @@ -0,0 +1,69 @@ +package model + +import ( + "github.com/shopspring/decimal" +) + +type GamePlayerScore struct { + GameID string `bun:"game_id,pk" json:"gameID"` + PlayerSlug string `bun:"player_slug,pk" json:"playerSlug"` + Score decimal.Decimal `bun:"score" json:"score"` + DecisiveScore decimal.Decimal `bun:"decisive_score" json:"decisiveScore"` + AllAroundScore decimal.Decimal `bun:"all_around_score" json:"allAroundScore"` + MinutesPlayed int `bun:"minutes_played" json:"minutesPlayed"` + GameStarted bool `bun:"game_started" json:"gameStarted"` + FormationPlace int `bun:"formation_place" json:"formationPlace"` + Live bool `bun:"live" json:"live"` + OnGameSheet bool `bun:"on_game_sheet" json:"onGameSheet"` + Reviewed bool `bun:"reviewed" json:"reviewed"` + Goal int `bun:"goal" json:"goal"` + Assist int `bun:"assist" json:"assist"` + PenaltyWon int `bun:"penalty_won" json:"penaltyWon"` + ClearanceOffLine int `bun:"clearance_off_line" json:"clearanceOffLine"` + LastManTackle int `bun:"last_man_tackle" json:"lastManTackle"` + PenaltySave int `bun:"penalty_save" json:"penaltySave"` + OwnGoal int `bun:"own_goal" json:"ownGoal"` + RedCard bool `bun:"red_card" json:"redCard"` + ErrorLeadToGoal int `bun:"error_lead_to_goal" json:"errorLeadToGoal"` + PenaltyConceded int `bun:"penalty_conceded" json:"penaltyConceded"` + YellowCard int `bun:"yellow_card" json:"yellowCard"` + Fouls int `bun:"fouls" json:"fouls"` + Fouled int `bun:"fouled" json:"fouled"` + CleanSheet bool `bun:"clean_sheet" json:"cleanSheet"` + DoubleDouble bool `bun:"double_double" json:"doubleDouble"` + TripleDouble bool `bun:"triple_double" json:"tripleDouble"` + TripleTriple bool `bun:"triple_triple" json:"tripleTriple"` + ErrorLeadToShot int `bun:"error_lead_to_shot" json:"errorLeadToShot"` + Saves int `bun:"saves" json:"saves"` + SavedShotFromInsideBox int `bun:"saved_shot_from_inside_box" json:"savedShotFromInsideBox"` + GoodHighClaim int `bun:"good_high_claim" json:"goodHighClaim"` + Punches int `bun:"punches" json:"punches"` + DivingSave int `bun:"diving_save" json:"divingSave"` + DivingCatch int `bun:"diving_catch" json:"divingCatch"` + CrossNotClaimed int `bun:"cross_not_claimed" json:"crossNotClaimed"` + GoalkeeperSmother int `bun:"goalkeeper_smother" json:"goalkeeperSmother"` + SixSecondViolation int `bun:"six_second_violation" json:"sixSecondViolation"` + KeeperSweeper int `bun:"keeper_sweeper" json:"keeperSweeper"` + GoalsConceded int `bun:"goals_conceded" json:"goalsConceded"` + EffectiveClearance int `bun:"effective_clearance" json:"effectiveClearance"` + WonTackle int `bun:"won_tackle" json:"wonTackle"` + BlockedCross int `bun:"blocked_cross" json:"blockedCross"` + Block int `bun:"block" json:"block"` + PossessionLost int `bun:"possession_lost" json:"possessionLost"` + PossessionWon int `bun:"possession_won" json:"possessionWon"` + DuelLost int `bun:"duel_lost" json:"duelLost"` + DuelWon int `bun:"duel_won" json:"duelWon"` + Interception int `bun:"interception" json:"interception"` + AccuratePass int `bun:"accurate_pass" json:"accuratePass"` + AccurateFinalThirdPass int `bun:"accurate_final_third_pass" json:"accurateFinalThirdPass"` + AccurateLongBall int `bun:"accurate_long_ball" json:"accurateLongBall"` + LongPassIntoOpposition int `bun:"long_pass_into_opposition" json:"longPassIntoOpposition"` + MissedPass int `bun:"missed_pass" json:"missedPass"` + ShotOnTarget int `bun:"shot_on_target" json:"shotOnTarget"` + WonContest int `bun:"won_contest" json:"wonContest"` + BigChanceCreated int `bun:"big_chance_created" json:"bigChanceCreated"` + AttemptedAssist int `bun:"attempted_assist" json:"attemptedAssist"` + PenaltyAreaEntries int `bun:"penalty_area_entries" json:"penaltyAreaEntries"` + PenaltyKickMissed int `bun:"penalty_kick_missed" json:"penaltyKickMissed"` + BigChanceMissed int `bun:"big_chance_missed" json:"bigChanceMissed"` +} diff --git a/model/game_player_score.sql.go b/model/game_player_score.sql.go deleted file mode 100644 index d777741..0000000 --- a/model/game_player_score.sql.go +++ /dev/null @@ -1,8 +0,0 @@ -// Code generated by sqlc. DO NOT EDIT. -// versions: -// sqlc v1.25.0 -// source: game_player_score.sql - -package model - -import () diff --git a/model/membership.go b/model/membership.go new file mode 100644 index 0000000..3f64da8 --- /dev/null +++ b/model/membership.go @@ -0,0 +1,17 @@ +package model + +import ( + "time" +) + +type Membership struct { + Id string `bun:"id,pk" json:"id"` + PlayerSlug string `bun:"player_slug,notnull" json:"playerSlug"` + TeamSlug string `bun:"team_slug,notnull" json:"teamSlug"` + StartDate time.Time `bun:"start_date,notnull" json:"startDate"` + EndDate *time.Time `bun:"end_date" json:"endDate"` + MembershipType string `bun:"membership_type" json:"membershipType"` + + Player Player `bun:"rel:has-one,join:player_slug=slug" json:"player"` + Team *Team `bun:"rel:has-one,join:team_slug=slug" json:"team"` +} diff --git a/model/models.go b/model/models.go deleted file mode 100644 index 5f58680..0000000 --- a/model/models.go +++ /dev/null @@ -1,164 +0,0 @@ -// Code generated by sqlc. DO NOT EDIT. -// versions: -// sqlc v1.25.0 - -package model - -import ( - "time" - - "github.com/jackc/pgx/v5/pgtype" - "github.com/shopspring/decimal" -) - -type Competition struct { - Slug string - DisplayName string - CountrySlug string - CompetitionFormat string - CompetitionType string - PictureUrl string - LogoUrl string - ZoneID *int32 -} - -type Country struct { - Slug string - Code string - DisplayName string - ThreeLetterCode string - FlagFlat64Url string - FlagFlat32Url string - FlagRound64Url string - FlagRound32Url string -} - -type Fixture struct { - Slug string - DisplayName string - State string - StartDate time.Time - EndDate time.Time - GameWeek int32 -} - -type Game struct { - ID string - Date pgtype.Timestamptz - CoverageStatus string - LowCoverage bool - Minutes int32 - PeriodType string - Scored bool - Status string - CompetitionSlug string - FixtureSlug string - AwayTeamSlug string - AwayGoals int32 - AwayExtraTimeScore int32 - AwayPenaltyScore int32 - HomeTeamSlug string - HomeGoals int32 - HomeExtraTimeScore int32 - HomePenaltyScore int32 - WinnerTeamSlug *string -} - -type GamePlayer struct { - GameID string - PlayerSlug string - Status string - TeamSlug string -} - -type GamePlayerScore struct { - GameID string - PlayerSlug string - Score decimal.Decimal - DecisiveScore decimal.Decimal - AllAroundScore decimal.Decimal - MinutesPlayed int32 - GameStarted bool - FormationPlace int32 - Live bool - OnGameSheet bool - Reviewed bool - Goal int32 - Assist int32 - PenaltyWon int32 - ClearanceOffLine int32 - LastManTackle int32 - PenaltySave int32 - OwnGoal int32 - RedCard bool - ErrorLeadToGoal int32 - PenaltyConceded int32 - YellowCard int32 - Fouls int32 - Fouled int32 - CleanSheet bool - DoubleDouble bool - TripleDouble bool - TripleTriple bool - ErrorLeadToShot int32 - Saves int32 - SavedShotFromInsideBox int32 - GoodHighClaim int32 - Punches int32 - DivingSave int32 - DivingCatch int32 - CrossNotClaimed int32 - GoalkeeperSmother int32 - SixSecondViolation int32 - KeeperSweeper int32 - GoalsConceded int32 - EffectiveClearance int32 - WonTackle int32 - BlockedCross int32 - Block int32 - PossessionLost int32 - PossessionWon int32 - DuelLost int32 - DuelWon int32 - Interception int32 - AccuratePass int32 - AccurateFinalThirdPass int32 - AccurateLongBall int32 - LongPassIntoOpposition int32 - MissedPass int32 - ShotOnTarget int32 - WonContest int32 - BigChanceCreated int32 - AttemptedAssist int32 - PenaltyAreaEntries int32 - PenaltyKickMissed int32 - BigChanceMissed int32 -} - -type Player struct { - Slug string - DisplayName string - BirthDate time.Time - CountrySlug string - TeamSlug *string - DomesticLeagueSlug *string - AvatarUrl string - FieldPosition string - Status string - ShirtNumber int32 -} - -type Team struct { - Slug string - DisplayName string - CountrySlug string - DomesticLeagueSlug *string - ShortName string - PictureUrl string - TeamType string -} - -type Zone struct { - ID int32 - DisplayName string -} diff --git a/model/player.go b/model/player.go new file mode 100644 index 0000000..37408ce --- /dev/null +++ b/model/player.go @@ -0,0 +1,25 @@ +package model + +import ( + "time" +) + +type Player struct { + Slug string `bun:"slug,pk" json:"slug"` + DisplayName string `bun:"display_name" json:"displayName"` + BirthDate time.Time `bun:"birth_date" json:"birthDate"` + CountrySlug string `bun:"country_slug" json:"countrySlug"` + TeamSlug *string `bun:"team_slug" json:"teamSlug"` + DomesticLeagueSlug *string `bun:"domestic_league_slug" json:"domesticLeagueSlug"` + AvatarUrl string `bun:"avatar_url" json:"avatarUrl"` + FieldPosition string `bun:"field_position" json:"fieldPosition"` + Status string `bun:"status" json:"status"` + ShirtNumber int `bun:"shirt_number" json:"shirtNumber"` + ActiveNationalTeamSlug *string `bun:"active_national_team_slug" json:"activeNationalTeamSlug"` + + Country *Country `bun:"rel:has-one,join:country_slug=slug" json:"country"` + Team *Team `bun:"rel:has-one,join:team_slug=slug" json:"team"` + DomesticLeague *Competition `bun:"rel:has-one,join:domestic_league_slug=slug" json:"domesticLeague"` + CardSupply []CardSupply `bun:"rel:has-many,join:slug=player_slug" json:"cardSupply"` + ClubMembership []Membership `bun:"rel:has-many,join:slug=player_slug" json:"clubMembership"` +} diff --git a/model/player.sql.go b/model/player.sql.go deleted file mode 100644 index dabd3b9..0000000 --- a/model/player.sql.go +++ /dev/null @@ -1,8 +0,0 @@ -// Code generated by sqlc. DO NOT EDIT. -// versions: -// sqlc v1.25.0 -// source: player.sql - -package model - -import () diff --git a/model/sql/competition.sql b/model/sql/competition.sql deleted file mode 100644 index fad7167..0000000 --- a/model/sql/competition.sql +++ /dev/null @@ -1,39 +0,0 @@ --- name: CreateOrUpdateCompetitions :batchexec - INSERT INTO competitions ( - slug, - display_name, - country_slug, - competition_format, - competition_type, - picture_url, - logo_url - ) - VALUES ($1, $2, $3, $4, $5, $6, $7) ON CONFLICT (slug) - DO - UPDATE - SET display_name = EXCLUDED.display_name, - competition_format = EXCLUDED.competition_format, - competition_type = EXCLUDED.competition_type, - picture_url = EXCLUDED.picture_url, - logo_url = EXCLUDED.logo_url, - country_slug = EXCLUDED.country_slug; - --- name: GetCompetitionBySlug :one - SELECT sqlc.embed(competitions), - sqlc.embed(zones), - sqlc.embed(countries) - FROM competitions - LEFT JOIN zones ON competitions.zone_id = zones.id - LEFT JOIN countries ON competitions.country_slug = countries.slug - WHERE competitions.slug = $1; - - --- name: GetAllCompetitions :many - SELECT sqlc.embed(competitions), - sqlc.embed(zones), - sqlc.embed(countries) - FROM competitions - LEFT JOIN zones ON competitions.zone_id = zones.id - LEFT JOIN countries ON competitions.country_slug = countries.slug; - - diff --git a/model/sql/country.sql b/model/sql/country.sql deleted file mode 100644 index 5a3186e..0000000 --- a/model/sql/country.sql +++ /dev/null @@ -1,49 +0,0 @@ --- name: CreateOrUpdateCountries :batchexec -INSERT INTO countries ( - slug, - code, - display_name, - three_letter_code, - flag_flat_64_url, - flag_flat_32_url, - flag_round_64_url, - flag_round_32_url - ) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8) - ON CONFLICT (slug) - DO UPDATE - SET code = EXCLUDED.code, - display_name = EXCLUDED.display_name, - three_letter_code = EXCLUDED.three_letter_code, - flag_flat_64_url = EXCLUDED.flag_flat_64_url, - flag_flat_32_url = EXCLUDED.flag_flat_32_url, - flag_round_64_url = EXCLUDED.flag_round_64_url, - flag_round_32_url = EXCLUDED.flag_round_32_url; - --- name: CreateOrUpdateCountry :exec - INSERT INTO countries ( - slug, - code, - display_name, - three_letter_code, - flag_flat_64_url, - flag_flat_32_url, - flag_round_64_url, - flag_round_32_url - ) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8) ON CONFLICT (slug) - DO - UPDATE - SET code = EXCLUDED.code, - display_name = EXCLUDED.display_name, - three_letter_code = EXCLUDED.three_letter_code, - flag_flat_64_url = EXCLUDED.flag_flat_64_url, - flag_flat_32_url = EXCLUDED.flag_flat_32_url, - flag_round_64_url = EXCLUDED.flag_round_64_url, - flag_round_32_url = EXCLUDED.flag_round_32_url; - - --- name: GetCountryBySlug :one - SELECT * - FROM countries - WHERE slug = $1; diff --git a/model/sql/fixture.sql b/model/sql/fixture.sql deleted file mode 100644 index 534cd68..0000000 --- a/model/sql/fixture.sql +++ /dev/null @@ -1,18 +0,0 @@ --- name: CreateOrUpdateFixtures :batchexec -INSERT INTO fixtures (slug, display_name, state, start_date, end_date, game_week) -VALUES ($1, $2, $3, $4, $5, $6) -ON CONFLICT (slug) DO UPDATE -SET display_name = $2, state = $3, start_date = $4, end_date = $5, game_week = $6; - --- name: GetFixtureBySlug :one -SELECT * FROM fixtures WHERE slug = $1; - --- name: GetFixtureByGameWeek :one -SELECT * FROM fixtures WHERE game_week = $1; - --- name: GetAllFixtures :many -SELECT * FROM fixtures; - - - - diff --git a/model/sql/game.sql b/model/sql/game.sql deleted file mode 100644 index 58fbf6e..0000000 --- a/model/sql/game.sql +++ /dev/null @@ -1,6 +0,0 @@ --- name: CreateOrUpdateGames :batchexec -INSERT INTO games (id, date, coverage_status, low_coverage, minutes, period_type, scored, status, competition_slug, fixture_slug, away_team_slug, away_goals, away_extra_time_score, away_penalty_score, home_team_slug, home_goals, home_extra_time_score, home_penalty_score, winner_team_slug) -VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19) -ON CONFLICT (id) DO UPDATE -SET date = EXCLUDED.date, coverage_status = EXCLUDED.coverage_status, low_coverage = EXCLUDED.low_coverage, minutes = EXCLUDED.minutes, period_type = EXCLUDED.period_type, scored = EXCLUDED.scored, status = EXCLUDED.status, competition_slug = EXCLUDED.competition_slug, fixture_slug = EXCLUDED.fixture_slug, away_team_slug = EXCLUDED.away_team_slug, away_goals = EXCLUDED.away_goals, away_extra_time_score = EXCLUDED.away_extra_time_score, away_penalty_score = EXCLUDED.away_penalty_score, home_team_slug = EXCLUDED.home_team_slug, home_goals = EXCLUDED.home_goals, home_extra_time_score = EXCLUDED.home_extra_time_score, home_penalty_score = EXCLUDED.home_penalty_score, winner_team_slug = EXCLUDED.winner_team_slug; - diff --git a/model/sql/game_player.sql b/model/sql/game_player.sql deleted file mode 100644 index bd0f66f..0000000 --- a/model/sql/game_player.sql +++ /dev/null @@ -1,15 +0,0 @@ --- name: CreateOrUpdateGamePlayers :batchexec -INSERT INTO game_players( - game_id, - player_slug, - status, - team_slug) -VALUES( - $1, - $2, - $3, - $4) - ON CONFLICT (game_id, player_slug) DO UPDATE - SET status = $3, - team_slug = $4; - diff --git a/model/sql/game_player_score.sql b/model/sql/game_player_score.sql deleted file mode 100644 index 887da06..0000000 --- a/model/sql/game_player_score.sql +++ /dev/null @@ -1,128 +0,0 @@ --- name: CreateOrUpdateGamePlayerScores :batchexec -INSERT INTO game_player_scores( - game_id, - player_slug, - score, - decisive_score, - all_around_score, - minutes_played, - game_started, - formation_place, - live, - on_game_sheet, - reviewed, - goal, - assist, - penalty_won, - clearance_off_line, - last_man_tackle, - penalty_save, - own_goal, - red_card, - error_lead_to_goal, - penalty_conceded, - yellow_card, - fouls, - fouled, - clean_sheet, - double_double, - triple_double, - triple_triple, - error_lead_to_shot, - saves, - saved_shot_from_inside_box, - good_high_claim, - punches, - diving_save, - diving_catch, - cross_not_claimed, - goalkeeper_smother, - six_second_violation, - keeper_sweeper, - goals_conceded, - effective_clearance, - won_tackle, - blocked_cross, - block, - possession_lost, - possession_won, - duel_lost, - duel_won, - interception, - accurate_pass, - accurate_final_third_pass, - accurate_long_ball, - long_pass_into_opposition, - missed_pass, - shot_on_target, - won_contest, - big_chance_created, - attempted_assist, - penalty_area_entries, - penalty_kick_missed, - big_chance_missed) -VALUES( - $1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24,$25,$26,$27,$28,$29,$30,$31, - $32,$33,$34,$35,$36,$37,$38,$39,$40,$41,$42,$43,$44,$45,$46,$47,$48,$49,$50,$51,$52,$53,$54,$55,$56,$57,$58,$59,$60, - $61) -ON CONFLICT (game_id, player_slug) DO UPDATE -SET score = EXCLUDED.score, - decisive_score = EXCLUDED.decisive_score, - all_around_score = EXCLUDED.all_around_score, - minutes_played = EXCLUDED.minutes_played, - game_started = EXCLUDED.game_started, - formation_place = EXCLUDED.formation_place, - live = EXCLUDED.live, - on_game_sheet = EXCLUDED.on_game_sheet, - reviewed = EXCLUDED.reviewed, - goal = EXCLUDED.goal, - assist = EXCLUDED.assist, - penalty_won = EXCLUDED.penalty_won, - clearance_off_line = EXCLUDED.clearance_off_line, - last_man_tackle = EXCLUDED.last_man_tackle, - penalty_save = EXCLUDED.penalty_save, - own_goal = EXCLUDED.own_goal, - red_card = EXCLUDED.red_card, - error_lead_to_goal = EXCLUDED.error_lead_to_goal, - penalty_conceded = EXCLUDED.penalty_conceded, - yellow_card = EXCLUDED.yellow_card, - fouls = EXCLUDED.fouls, - fouled = EXCLUDED.fouled, - clean_sheet = EXCLUDED.clean_sheet, - double_double = EXCLUDED.double_double, - triple_double = EXCLUDED.triple_double, - triple_triple = EXCLUDED.triple_triple, - error_lead_to_shot = EXCLUDED.error_lead_to_shot, - saves = EXCLUDED.saves, - saved_shot_from_inside_box = EXCLUDED.saved_shot_from_inside_box, - good_high_claim = EXCLUDED.good_high_claim, - punches = EXCLUDED.punches, - diving_save = EXCLUDED.diving_save, - diving_catch = EXCLUDED.diving_catch, - cross_not_claimed = EXCLUDED.cross_not_claimed, - goalkeeper_smother = EXCLUDED.goalkeeper_smother, - six_second_violation = EXCLUDED.six_second_violation, - keeper_sweeper = EXCLUDED.keeper_sweeper, - goals_conceded = EXCLUDED.goals_conceded, - effective_clearance = EXCLUDED.effective_clearance, - won_tackle = EXCLUDED.won_tackle, - blocked_cross = EXCLUDED.blocked_cross, - block = EXCLUDED.block, - possession_lost = EXCLUDED.possession_lost, - possession_won = EXCLUDED.possession_won, - duel_lost = EXCLUDED.duel_lost, - duel_won = EXCLUDED.duel_won, - interception = EXCLUDED.interception, - accurate_pass = EXCLUDED.accurate_pass, - accurate_final_third_pass = EXCLUDED.accurate_final_third_pass, - accurate_long_ball = EXCLUDED.accurate_long_ball, - long_pass_into_opposition = EXCLUDED.long_pass_into_opposition, - missed_pass = EXCLUDED.missed_pass, - shot_on_target = EXCLUDED.shot_on_target, - won_contest = EXCLUDED.won_contest, - big_chance_created = EXCLUDED.big_chance_created, - attempted_assist = EXCLUDED.attempted_assist, - penalty_area_entries = EXCLUDED.penalty_area_entries, - penalty_kick_missed = EXCLUDED.penalty_kick_missed, - big_chance_missed = EXCLUDED.big_chance_missed; - diff --git a/model/sql/player.sql b/model/sql/player.sql deleted file mode 100644 index 40de348..0000000 --- a/model/sql/player.sql +++ /dev/null @@ -1,5 +0,0 @@ --- name: CreateOrUpdatePlayers :batchexec -INSERT INTO players (slug, display_name, birth_date, country_slug, team_slug, domestic_league_slug, avatar_url, field_position, status, shirt_number) -VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) -ON CONFLICT (slug) DO UPDATE -SET display_name = $2, birth_date = $3, country_slug = $4, team_slug = $5, domestic_league_slug = $6, avatar_url = $7, field_position = $8, status = $9, shirt_number = $10; diff --git a/model/sql/team.sql b/model/sql/team.sql deleted file mode 100644 index ad31dd0..0000000 --- a/model/sql/team.sql +++ /dev/null @@ -1,5 +0,0 @@ --- name: CreateOrUpdateTeams :batchexec -INSERT INTO teams (slug, display_name, country_slug, domestic_league_slug, short_name, picture_url, team_type) -VALUES ($1, $2, $3, $4, $5, $6, $7) -ON CONFLICT (slug) DO UPDATE -SET display_name = $2, country_slug = $3, domestic_league_slug = $4, short_name = $5, picture_url = $6, team_type = $7; diff --git a/model/sql/zone.sql b/model/sql/zone.sql deleted file mode 100644 index d844a2d..0000000 --- a/model/sql/zone.sql +++ /dev/null @@ -1,2 +0,0 @@ --- name: CountZones :one -SELECT COUNT(*) FROM zones; \ No newline at end of file diff --git a/model/team.go b/model/team.go new file mode 100644 index 0000000..70d252a --- /dev/null +++ b/model/team.go @@ -0,0 +1,14 @@ +package model + +type Team struct { + Slug string `bun:"slug,pk" json:"slug"` + DisplayName string `bun:"display_name" json:"displayName"` + CountrySlug string `bun:"country_slug" json:"countrySlug"` + DomesticLeagueSlug *string `bun:"domestic_league_slug" json:"domesticLeagueSlug"` + ShortName string `bun:"short_name" json:"shortName"` + PictureUrl string `bun:"picture_url" json:"pictureUrl"` + TeamType string `bun:"team_type" json:"teamType"` + + Country *Country `bun:"rel:has-one,join:country_slug=slug" json:"country"` + DomesticLeague *Competition `bun:"rel:has-one,join:domestic_league_slug=slug" json:"domesticLeague"` +} diff --git a/model/team.sql.go b/model/team.sql.go deleted file mode 100644 index 1705334..0000000 --- a/model/team.sql.go +++ /dev/null @@ -1,8 +0,0 @@ -// Code generated by sqlc. DO NOT EDIT. -// versions: -// sqlc v1.25.0 -// source: team.sql - -package model - -import () diff --git a/model/zone.go b/model/zone.go new file mode 100644 index 0000000..3172a88 --- /dev/null +++ b/model/zone.go @@ -0,0 +1,6 @@ +package model + +type Zone struct { + Id int `bun:"id,pk" json:"id"` + DisplayName string `bun:"display_name" json:"displayName"` +} diff --git a/model/zone.sql.go b/model/zone.sql.go deleted file mode 100644 index c110f6d..0000000 --- a/model/zone.sql.go +++ /dev/null @@ -1,21 +0,0 @@ -// Code generated by sqlc. DO NOT EDIT. -// versions: -// sqlc v1.25.0 -// source: zone.sql - -package model - -import ( - "context" -) - -const countZones = `-- name: CountZones :one -SELECT COUNT(*) FROM zones -` - -func (q *Queries) CountZones(ctx context.Context) (int64, error) { - row := q.db.QueryRow(ctx, countZones) - var count int64 - err := row.Scan(&count) - return count, err -} diff --git a/sorare_utils/game.go b/sorare_utils/game.go index ac9577a..1f025da 100644 --- a/sorare_utils/game.go +++ b/sorare_utils/game.go @@ -2,31 +2,30 @@ package sorare_utils import ( "git.lehouerou.net/laurent/sorare/football" - "github.com/jackc/pgx/v5/pgtype" "git.lehouerou.net/laurent/sorarebuddy/model" ) -func NewCreateOrUpdateGamesParamsFromSorare(game football.Game) model.CreateOrUpdateGamesParams { - return model.CreateOrUpdateGamesParams{ - ID: game.Id.Value, - Date: pgtype.Timestamptz{Time: game.Date, Valid: true}, +func NewGameFromSorare(game football.Game) model.Game { + return model.Game{ + Id: game.Id.Value, + Date: game.Date, CoverageStatus: game.CoverageStatus, LowCoverage: game.LowCoverage, - Minutes: int32(game.Minute), + Minutes: int(game.Minute), PeriodType: game.PeriodType, Scored: game.Scored, Status: game.Status, CompetitionSlug: game.Competition.Slug, FixtureSlug: game.So5Fixture.Slug, AwayTeamSlug: game.AwayTeam.Team.Slug, - AwayGoals: int32(game.AwayGoals), - AwayExtraTimeScore: int32(game.ExtraTimeScoreAway), - AwayPenaltyScore: int32(game.PenaltyScoreAway), + AwayGoals: int(game.AwayGoals), + AwayExtraTimeScore: int(game.ExtraTimeScoreAway), + AwayPenaltyScore: int(game.PenaltyScoreAway), HomeTeamSlug: game.HomeTeam.Team.Slug, - HomeGoals: int32(game.HomeGoals), - HomeExtraTimeScore: int32(game.ExtraTimeScoreHome), - HomePenaltyScore: int32(game.PenaltyScoreHome), + HomeGoals: int(game.HomeGoals), + HomeExtraTimeScore: int(game.ExtraTimeScoreHome), + HomePenaltyScore: int(game.PenaltyScoreHome), WinnerTeamSlug: func() *string { if game.Winner.Team.Slug == "" { return nil diff --git a/sorare_utils/game_player.go b/sorare_utils/game_player.go deleted file mode 100644 index 1a752d8..0000000 --- a/sorare_utils/game_player.go +++ /dev/null @@ -1,52 +0,0 @@ -package sorare_utils - -import ( - "git.lehouerou.net/laurent/sorare/football" - - "git.lehouerou.net/laurent/sorarebuddy/model" -) - -func ExtractPlayersFromGameWithFormation( - gameWithFormation football.GameWithFormation, -) []model.CreateOrUpdateGamePlayersParams { - var res []model.CreateOrUpdateGamePlayersParams - for _, p := range gameWithFormation.HomeFormation.Bench { - res = append(res, model.CreateOrUpdateGamePlayersParams{ - GameID: gameWithFormation.Id.Value, - PlayerSlug: p.Slug, - TeamSlug: gameWithFormation.HomeTeam.Team.Slug, - Status: "bench", - }) - } - for _, p := range gameWithFormation.HomeFormation.StartingLineup { - for _, q := range p { - res = append(res, model.CreateOrUpdateGamePlayersParams{ - GameID: gameWithFormation.Id.Value, - PlayerSlug: q.Slug, - TeamSlug: gameWithFormation.HomeTeam.Team.Slug, - Status: "starting", - }) - } - } - for _, p := range gameWithFormation.AwayFormation.Bench { - res = append(res, model.CreateOrUpdateGamePlayersParams{ - GameID: gameWithFormation.Id.Value, - PlayerSlug: p.Slug, - TeamSlug: gameWithFormation.AwayTeam.Team.Slug, - Status: "bench", - }) - } - for _, p := range gameWithFormation.AwayFormation.StartingLineup { - for _, q := range p { - res = append(res, model.CreateOrUpdateGamePlayersParams{ - GameID: gameWithFormation.Id.Value, - PlayerSlug: q.Slug, - TeamSlug: gameWithFormation.AwayTeam.Team.Slug, - Status: "starting", - }) - } - } - - return res - -} diff --git a/sorare_utils/game_player_score.go b/sorare_utils/game_player_score.go index 74d5958..f61cee4 100644 --- a/sorare_utils/game_player_score.go +++ b/sorare_utils/game_player_score.go @@ -7,18 +7,17 @@ import ( "git.lehouerou.net/laurent/sorarebuddy/model" ) -func NewCreateOrUpdateGamePlayerScoresParamsFromSorare( +func NewGamePlayerScoreFromSorare( gameId string, - playerScore football.PlayerScore, -) model.CreateOrUpdateGamePlayerScoresParams { - s := playerScore.So5Score + s football.So5Score, +) model.GamePlayerScore { - res := model.CreateOrUpdateGamePlayerScoresParams{ + res := model.GamePlayerScore{ GameID: gameId, PlayerSlug: s.Player.Slug, - MinutesPlayed: int32(s.PlayerGameStats.MinsPlayed), + MinutesPlayed: int(s.PlayerGameStats.MinsPlayed), GameStarted: s.PlayerGameStats.GameStarted == 1, - FormationPlace: int32(s.PlayerGameStats.FormationPlace), + FormationPlace: int(s.PlayerGameStats.FormationPlace), Live: s.PlayerGameStats.Live, OnGameSheet: s.PlayerGameStats.OnGameSheet, Reviewed: s.PlayerGameStats.Reviewed, @@ -30,11 +29,11 @@ func NewCreateOrUpdateGamePlayerScoresParamsFromSorare( for _, stat := range s.AllAroundStats { switch stat.Stat { case "yellow_card": - res.YellowCard = int32(stat.StatValue.IntPart()) + res.YellowCard = int(stat.StatValue.IntPart()) case "fouls": - res.Fouls = int32(stat.StatValue.IntPart()) + res.Fouls = int(stat.StatValue.IntPart()) case "was_fouled": - res.Fouled = int32(stat.StatValue.IntPart()) + res.Fouled = int(stat.StatValue.IntPart()) case "clean_sheet_60": res.CleanSheet = int(stat.StatValue.IntPart()) == 1 case "double_double": @@ -44,99 +43,99 @@ func NewCreateOrUpdateGamePlayerScoresParamsFromSorare( case "triple_triple": res.TripleTriple = int(stat.StatValue.IntPart()) == 1 case "error_lead_to_shot": - res.ErrorLeadToShot = int32(stat.StatValue.IntPart()) + res.ErrorLeadToShot = int(stat.StatValue.IntPart()) case "saves": - res.Saves = int32(stat.StatValue.IntPart()) + res.Saves = int(stat.StatValue.IntPart()) case "saved_ibox": - res.SavedShotFromInsideBox = int32(stat.StatValue.IntPart()) + res.SavedShotFromInsideBox = int(stat.StatValue.IntPart()) case "good_high_claim": - res.GoodHighClaim = int32(stat.StatValue.IntPart()) + res.GoodHighClaim = int(stat.StatValue.IntPart()) case "punches": - res.Punches = int32(stat.StatValue.IntPart()) + res.Punches = int(stat.StatValue.IntPart()) case "dive_save": - res.DivingSave = int32(stat.StatValue.IntPart()) + res.DivingSave = int(stat.StatValue.IntPart()) case "dive_catch": - res.DivingCatch = int32(stat.StatValue.IntPart()) + res.DivingCatch = int(stat.StatValue.IntPart()) case "cross_not_claimed": - res.CrossNotClaimed = int32(stat.StatValue.IntPart()) + res.CrossNotClaimed = int(stat.StatValue.IntPart()) case "six_second_violation": - res.SixSecondViolation = int32(stat.StatValue.IntPart()) + res.SixSecondViolation = int(stat.StatValue.IntPart()) case "gk_smother": - res.GoalkeeperSmother = int32(stat.StatValue.IntPart()) + res.GoalkeeperSmother = int(stat.StatValue.IntPart()) case "accurate_keeper_sweeper": - res.KeeperSweeper = int32(stat.StatValue.IntPart()) + res.KeeperSweeper = int(stat.StatValue.IntPart()) case "goals_conceded": - res.GoalsConceded = int32(stat.StatValue.IntPart()) + res.GoalsConceded = int(stat.StatValue.IntPart()) case "effective_clearance": - res.EffectiveClearance = int32(stat.StatValue.IntPart()) + res.EffectiveClearance = int(stat.StatValue.IntPart()) case "won_tackle": - res.WonTackle = int32(stat.StatValue.IntPart()) + res.WonTackle = int(stat.StatValue.IntPart()) tacklefound = true case "blocked_cross": - res.BlockedCross = int32(stat.StatValue.IntPart()) + res.BlockedCross = int(stat.StatValue.IntPart()) case "outfielder_block": - res.Block = int32(stat.StatValue.IntPart()) + res.Block = int(stat.StatValue.IntPart()) case "poss_lost_ctrl": - res.PossessionLost = int32(stat.StatValue.IntPart()) + res.PossessionLost = int(stat.StatValue.IntPart()) case "poss_won": - res.PossessionWon = int32(stat.StatValue.IntPart()) + res.PossessionWon = int(stat.StatValue.IntPart()) case "duel_lost": - res.DuelLost = int32(stat.StatValue.IntPart()) + res.DuelLost = int(stat.StatValue.IntPart()) case "duel_won": - res.DuelWon = int32(stat.StatValue.IntPart()) + res.DuelWon = int(stat.StatValue.IntPart()) case "interception_won": - res.Interception = int32(stat.StatValue.IntPart()) + res.Interception = int(stat.StatValue.IntPart()) case "accurate_pass": - res.AccuratePass = int32(stat.StatValue.IntPart()) + res.AccuratePass = int(stat.StatValue.IntPart()) case "successful_final_third_passes": - res.AccurateFinalThirdPass = int32(stat.StatValue.IntPart()) + res.AccurateFinalThirdPass = int(stat.StatValue.IntPart()) case "accurate_long_balls": - res.AccurateLongBall = int32(stat.StatValue.IntPart()) + res.AccurateLongBall = int(stat.StatValue.IntPart()) case "long_pass_own_to_opp_success": - res.LongPassIntoOpposition = int32(stat.StatValue.IntPart()) + res.LongPassIntoOpposition = int(stat.StatValue.IntPart()) case "ontarget_scoring_att": - res.ShotOnTarget = int32(stat.StatValue.IntPart()) + res.ShotOnTarget = int(stat.StatValue.IntPart()) case "won_contest": - res.WonContest = int32(stat.StatValue.IntPart()) + res.WonContest = int(stat.StatValue.IntPart()) case "pen_area_entries": - res.PenaltyAreaEntries = int32(stat.StatValue.IntPart()) + res.PenaltyAreaEntries = int(stat.StatValue.IntPart()) case "big_chance_created": - res.BigChanceCreated = int32(stat.StatValue.IntPart()) + res.BigChanceCreated = int(stat.StatValue.IntPart()) case "adjusted_total_att_assist": - res.AttemptedAssist = int32(stat.StatValue.IntPart()) + res.AttemptedAssist = int(stat.StatValue.IntPart()) case "penalty_kick_missed": - res.PenaltyKickMissed = int32(stat.StatValue.IntPart()) + res.PenaltyKickMissed = int(stat.StatValue.IntPart()) case "big_chance_missed": - res.BigChanceMissed = int32(stat.StatValue.IntPart()) + res.BigChanceMissed = int(stat.StatValue.IntPart()) } allAroundScore = allAroundScore.Add(stat.TotalScore) } - res.MissedPass = int32(s.PlayerGameStats.TotalPass - s.PlayerGameStats.AccuratePass) + res.MissedPass = int(s.PlayerGameStats.TotalPass - s.PlayerGameStats.AccuratePass) if !tacklefound { - res.WonTackle = int32(s.PlayerGameStats.TotalTackle) + res.WonTackle = int(s.PlayerGameStats.TotalTackle) } for _, stat := range s.PositiveDecisiveStats { switch stat.Stat { case "goals": - res.Goal = int32(stat.StatValue.IntPart()) + res.Goal = int(stat.StatValue.IntPart()) case "goal_assist": - res.Assist = int32(stat.StatValue.IntPart()) + res.Assist = int(stat.StatValue.IntPart()) case "assist_penalty_won": - res.PenaltyWon = int32(stat.StatValue.IntPart()) + res.PenaltyWon = int(stat.StatValue.IntPart()) case "clearance_off_line": - res.ClearanceOffLine = int32(stat.StatValue.IntPart()) + res.ClearanceOffLine = int(stat.StatValue.IntPart()) case "last_man_tackle": - res.LastManTackle = int32(stat.StatValue.IntPart()) + res.LastManTackle = int(stat.StatValue.IntPart()) case "clean_sheet_60": - res.CleanSheet = int32(stat.StatValue.IntPart()) == 1 + res.CleanSheet = int(stat.StatValue.IntPart()) == 1 case "penalty_save": - res.PenaltySave = int32(stat.StatValue.IntPart()) + res.PenaltySave = int(stat.StatValue.IntPart()) } } @@ -145,11 +144,11 @@ func NewCreateOrUpdateGamePlayerScoresParamsFromSorare( case "red_card": res.RedCard = int(stat.StatValue.IntPart()) == 1 case "own_goals": - res.OwnGoal = int32(stat.StatValue.IntPart()) + res.OwnGoal = int(stat.StatValue.IntPart()) case "error_lead_to_goal": - res.ErrorLeadToGoal = int32(stat.StatValue.IntPart()) + res.ErrorLeadToGoal = int(stat.StatValue.IntPart()) case "penalty_conceded": - res.PenaltyConceded = int32(stat.StatValue.IntPart()) + res.PenaltyConceded = int(stat.StatValue.IntPart()) } } diff --git a/sorare_utils/update_service.go b/sorare_utils/update_service.go index 591e721..2e764dc 100644 --- a/sorare_utils/update_service.go +++ b/sorare_utils/update_service.go @@ -2,512 +2,188 @@ package sorare_utils import ( "context" + "time" "git.lehouerou.net/laurent/sorare" "git.lehouerou.net/laurent/sorare/football" - "git.lehouerou.net/laurent/sorare/graphql" - "github.com/jackc/pgx/v5/pgtype" - gql "github.com/llehouerou/go-graphql-client" "github.com/pkg/errors" "github.com/rs/zerolog/log" "github.com/samber/lo" + "git.lehouerou.net/laurent/sorarebuddy/db" "git.lehouerou.net/laurent/sorarebuddy/model" ) type UpdateService struct { s *sorare.Sorare - db *model.Queries + db *db.Client + + updater *Updater } -func NewUpdateService(s *sorare.Sorare, db *model.Queries) *UpdateService { - return &UpdateService{s: s, db: db} +func NewUpdateService(s *sorare.Sorare, db *db.Client) *UpdateService { + return &UpdateService{s: s, db: db, updater: NewUpdater(s, db)} } -func (u *UpdateService) InitSyncDatabase(ctx context.Context) error { - - sfixtures, err := u.s.Football.So5.So5Fixtures.Get(ctx, football.So5FixturesParams{ - // AasmStates: []string{"started"}, - }) +func (u *UpdateService) updateAllFixtures(ctx context.Context) error { + log.Debug().Msg("### updating all fixtures ###") + fixtures, err := u.s.Football.So5.So5Fixtures.Get(ctx, football.So5FixturesParams{}) if err != nil { return err } - log.Debug().Msgf("fixtures: %v", sfixtures) + log.Debug().Msgf("found %d fixtures in sorare", len(fixtures)) - batchFixtures := u.db.CreateOrUpdateFixtures( + err = u.db.Fixtures.CreateOrUpdateMany( ctx, - lo.Map(sfixtures, func(fixture football.So5Fixture, index int) model.CreateOrUpdateFixturesParams { - return model.CreateOrUpdateFixturesParams{ + lo.Map(fixtures, func(fixture football.So5Fixture, index int) model.Fixture { + return model.Fixture{ Slug: fixture.Slug, DisplayName: fixture.DisplayName, State: fixture.AasmState, - StartDate: pgtype.Timestamptz{Time: fixture.StartDate, Valid: true}, - EndDate: pgtype.Timestamptz{Time: fixture.EndDate, Valid: true}, - GameWeek: int32(fixture.GameWeek), + StartDate: fixture.StartDate, + EndDate: fixture.EndDate, + GameWeek: fixture.GameWeek, } }), ) - var batcherr error - batchFixtures.Exec(func(_ int, err error) { - if err != nil { - batcherr = err - batchFixtures.Close() - - } - }) - if batcherr != nil { - return err - } - - log.Debug().Msgf("created %d fixtures", len(sfixtures)) - - fixtures, err := u.db.GetAllFixtures(ctx) if err != nil { - return err - } - games, err := GetGamesFromFixtures( - ctx, - u.s, - lo.Map(fixtures, func(fixture model.Fixture, index int) string { - return fixture.Slug - }), - ) - log.Info().Msgf("found %d games to process", len(games)) - - log.Debug().Msgf("getting players for each game...") - var gamePlayers []model.CreateOrUpdateGamePlayersParams - playerSlugsByGameMap := make(map[string][]string) - for _, game := range games { - gameWithFormation, err := u.s.Football.Game.Get(ctx, graphql.IdParams{Id: gql.ID(game.Id.Value)}) - if err != nil { - return errors.Wrapf(err, "getting game with formation %s", game.Id.Value) - } - newplayers := ExtractPlayersFromGameWithFormation(gameWithFormation) - log.Debug().Msgf("\t%s -> %d players", game.String(), len(newplayers)) - playerSlugsByGameMap[game.Id.Value] = lo.Map( - newplayers, - func(player model.CreateOrUpdateGamePlayersParams, index int) string { - return player.PlayerSlug - }, - ) - gamePlayers = append(gamePlayers, newplayers...) + return errors.Wrap(err, "inserting fixtures") } - playerSlugs := lo.Uniq( - lo.Filter(lo.Map(gamePlayers, func(player model.CreateOrUpdateGamePlayersParams, index int) string { - return player.PlayerSlug - }), func(slug string, index int) bool { - return slug != "" - }), - ) + log.Debug().Msgf("created %d fixtures", len(fixtures)) + return nil +} - log.Debug().Msgf("getting players...") - var players []football.Player - for i, chunk := range lo.Chunk(playerSlugs, 80) { - log.Debug().Msgf("\tbatch %d/%d", i+1, (len(playerSlugs)/80)+1) - p, err := u.s.Football.Players.Get(ctx, graphql.SlugsParams{Slugs: chunk}) - if err != nil { - return errors.Wrapf(err, "getting players batch %d", i) - } - players = append(players, p...) +func (u *UpdateService) UpdatePlayers(ctx context.Context, playerSlugs []string, opts ...UpdaterOption) error { + u.updater.Reset() + u.updater.AddPlayersToRead(playerSlugs...) + err := u.updater.Update(ctx, opts...) + if err != nil { + return errors.Wrap(err, "updating data") } - log.Debug().Msgf("found %d players", len(players)) + return nil +} - teamSlugs := ExtractTeamSlugsFromPlayersAndGames(players, games) - log.Debug().Msgf("extracted %d unique team slugs from games and players", len(teamSlugs)) - - log.Debug().Msgf("getting clubs...") - var clubs []football.Club - for i, chunk := range lo.Chunk(teamSlugs, 100) { - log.Debug().Msgf("\tbatch %d/%d", i+1, (len(teamSlugs)/100)+1) - t, err := u.s.Football.Clubs.Get(ctx, graphql.SlugsParams{Slugs: chunk}) - if err != nil { - return err - } - clubs = append(clubs, t...) +func (u *UpdateService) UpdateAllPlayers(ctx context.Context, opts ...UpdaterOption) error { + dbplayers, err := u.db.Players.GetAll(ctx) + if err != nil { + return errors.Wrap(err, "getting players from db") } - log.Debug().Msgf("found %d clubs", len(clubs)) - var nationalTeams []football.NationalTeam - slugsLeft := lo.Without(teamSlugs, lo.Map(clubs, func(club football.Club, index int) string { - return club.Slug + playerSlugs := lo.Map(dbplayers, func(player model.Player, index int) string { + return player.Slug + }) + return u.UpdatePlayers(ctx, playerSlugs, opts...) +} + +func (u *UpdateService) SyncStartedFixture(ctx context.Context, opts ...UpdaterOption) error { + f, err := u.db.Fixtures.GetStarted(ctx) + if err != nil { + return errors.Wrap(err, "getting started fixtures") + } + return u.SyncDatabaseForFixtures(ctx, f, opts...) +} + +func (u *UpdateService) SyncDatabaseForFixtures( + ctx context.Context, + fixtures []model.Fixture, + opts ...UpdaterOption, +) error { + + u.updater.Reset() + u.updater.AddGamesFromFixtureToRead(lo.Map(fixtures, func(fixture model.Fixture, index int) string { + return fixture.Slug })...) - log.Debug().Msgf("getting national teams...") - log.Debug().Msgf("slugs left: %d", len(slugsLeft)) - for i, chunk := range lo.Chunk(slugsLeft, 100) { - log.Debug().Msgf("\tbatch %d/%d", i+1, (len(teamSlugs)/100)+1) - t, err := u.s.Football.NationalTeams.Get(ctx, graphql.SlugsParams{Slugs: chunk}) - if err != nil { - return err - } - nationalTeams = append(nationalTeams, t...) + + err := u.updater.Update(ctx, opts...) + if err != nil { + return errors.Wrap(err, "updating data") } - log.Debug().Msgf("found %d national teams", len(nationalTeams)) - - competitionSlugs := ExtractCompetitionSlugsFromPlayersGamesAndClubs(players, games, clubs) - log.Debug().Msgf("extracted %d unique competition slugs from players, games and clubs", len(competitionSlugs)) - log.Debug().Msgf("getting competitions...") - var competitions []football.Competition - for _, slug := range competitionSlugs { - log.Debug().Msgf("\tcompetition %s", slug) - c, err := u.s.Football.Competition.Get(ctx, graphql.SlugParams{Slug: slug}) - if err != nil { - return err - } - competitions = append(competitions, c) - } - log.Debug().Msgf("found %d competitions", len(competitions)) - - countrySlugs := ExtractCountrySlugsFromPlayersCompetitionsClubsAndNationalTeams( - players, - competitions, - clubs, - nationalTeams, - ) - log.Debug(). - Msgf("extracted %d unique country slugs from players, competitions, clubs and national teams", len(countrySlugs)) - log.Debug().Msgf("getting countries...") - var countries []sorare.Country - for i, chunk := range lo.Chunk(countrySlugs, 100) { - log.Debug().Msgf("\tbatch %d/%d", i+1, (len(countrySlugs)/100)+1) - c, err := u.s.Countries.Get(ctx, graphql.SlugsParams{Slugs: chunk}) - if err != nil { - return err - } - countries = append(countries, c...) - } - log.Debug().Msgf("found %d countries", len(countries)) - - scores := make(map[string][]football.PlayerScore) - for gameId, playerSlugs := range playerSlugsByGameMap { - log.Debug().Msgf("getting scores for game %s...", gameId) - var gameScores []football.PlayerScore - for i, chunk := range lo.Chunk(playerSlugs, 80) { - log.Debug().Msgf("\tbatch %d/%d", i+1, (len(playerSlugs)/80)+1) - s, err := u.s.Football.PlayersGameScores(gql.ID(gameId)).Get(ctx, graphql.SlugsParams{Slugs: chunk}) - if err != nil { - return errors.Wrapf(err, "getting scores for game %s", gameId) - } - gameScores = append(gameScores, s...) - } - for _, score := range gameScores { - log.Debug().Msgf("\t%s -> %s", score.Slug, score.So5Score.Score) - } - scores[gameId] = gameScores - } - - log.Debug().Msg("inserting countries into db...") - batchCountries := u.db.CreateOrUpdateCountries( - ctx, - lo.Map(countries, func(country sorare.Country, index int) model.CreateOrUpdateCountriesParams { - return model.CreateOrUpdateCountriesParams{ - Slug: country.Slug, - Code: country.Code, - DisplayName: country.Name, - ThreeLetterCode: country.ThreeLetterCode, - FlagFlat64Url: country.FlagFlat64Url, - FlagFlat32Url: country.FlagFlat32Url, - FlagRound64Url: country.FlagRound64Url, - FlagRound32Url: country.FlagRound32Url, - } - }), - ) - batcherr = nil - batchCountries.Exec(func(_ int, err error) { - if err != nil { - batcherr = err - batchCountries.Close() - } - }) - if batcherr != nil { - return err - } - log.Debug().Msgf("%d countries inserted", len(countries)) - - log.Debug().Msg("inserting competitions into db...") - - batchCompetitions := u.db.CreateOrUpdateCompetitions( - ctx, - lo.Map(competitions, func(competition football.Competition, index int) model.CreateOrUpdateCompetitionsParams { - return model.CreateOrUpdateCompetitionsParams{ - Slug: competition.Slug, - CompetitionFormat: competition.Format, - CompetitionType: competition.Type, - DisplayName: competition.DisplayName, - PictureUrl: competition.PictureUrl, - LogoUrl: competition.LogoUrl, - CountrySlug: competition.Country.Slug, - } - }), - ) - batcherr = nil - batchCompetitions.Exec(func(_ int, err error) { - if err != nil { - batcherr = err - batchCompetitions.Close() - } - }) - if batcherr != nil { - return errors.Wrap(batcherr, "inserting competitions") - } - - log.Debug().Msgf("%d competitions inserted", len(competitions)) - - log.Debug().Msg("inserting teams into db...") - batchTeams := u.db.CreateOrUpdateTeams(ctx, lo.Union( - lo.Map(clubs, func(club football.Club, index int) model.CreateOrUpdateTeamsParams { - return model.CreateOrUpdateTeamsParams{ - Slug: club.Slug, - DisplayName: club.Name, - CountrySlug: club.Country.Slug, - DomesticLeagueSlug: func() *string { - if club.DomesticLeague.Slug == "" { - return nil - } - return &club.DomesticLeague.Slug - }(), - ShortName: club.ShortName, - PictureUrl: club.PictureUrl, - TeamType: "club", - } - }), - lo.Map(nationalTeams, func(nationalTeam football.NationalTeam, index int) model.CreateOrUpdateTeamsParams { - return model.CreateOrUpdateTeamsParams{ - Slug: nationalTeam.Slug, - DisplayName: nationalTeam.Name, - CountrySlug: nationalTeam.Country.Slug, - DomesticLeagueSlug: nil, - ShortName: nationalTeam.ShortName, - PictureUrl: nationalTeam.PictureUrl, - TeamType: "national", - } - }), - )) - batcherr = nil - batchTeams.Exec(func(_ int, err error) { - if err != nil { - batcherr = err - batchTeams.Close() - } - }) - if batcherr != nil { - return errors.Wrap(batcherr, "inserting teams into db") - } - log.Debug().Msgf("%d teams inserted", len(clubs)+len(nationalTeams)) - - log.Debug().Msg("inserting games into db...") - batchGames := u.db.CreateOrUpdateGames( - ctx, - lo.Map(games, func(game football.Game, index int) model.CreateOrUpdateGamesParams { - return NewCreateOrUpdateGamesParamsFromSorare(game) - }), - ) - batcherr = nil - batchGames.Exec(func(_ int, err error) { - if err != nil { - batcherr = err - batchGames.Close() - } - }) - if batcherr != nil { - return err - } - log.Debug().Msgf("%d games inserted", len(games)) - - log.Debug().Msg("inserting players into db...") - batchPlayers := u.db.CreateOrUpdatePlayers( - ctx, - lo.Map(players, func(player football.Player, index int) model.CreateOrUpdatePlayersParams { - res := model.CreateOrUpdatePlayersParams{ - Slug: player.Slug, - DisplayName: player.DisplayName, - BirthDate: player.BirthDate, - CountrySlug: player.Country.Slug, - AvatarUrl: player.AvatarUrl, - FieldPosition: string(player.Position), - Status: string(player.PlayingStatus), - ShirtNumber: int32(player.ShirtNumber), - } - for _, competition := range player.ActiveClub.ActiveCompetitions { - if competition.Format == "DOMESTIC_LEAGUE" { - res.DomesticLeagueSlug = &competition.Slug - } - } - if player.ActiveClub.Slug != "" { - res.TeamSlug = &player.ActiveClub.Slug - } - return res - }), - ) - batcherr = nil - batchPlayers.Exec(func(_ int, err error) { - if err != nil { - batcherr = err - batchPlayers.Close() - } - }) - if batcherr != nil { - return errors.Wrap(batcherr, "inserting players") - } - log.Debug().Msgf("%d players inserted", len(players)) - - log.Debug().Msg("inserting game players into db...") - batchGamePlayers := u.db.CreateOrUpdateGamePlayers( - ctx, - gamePlayers, - ) - batcherr = nil - batchGamePlayers.Exec(func(_ int, err error) { - if err != nil { - batcherr = err - batchGamePlayers.Close() - } - }) - if batcherr != nil { - return errors.Wrap(err, "inserting game players") - } - log.Debug().Msgf("%d game players inserted", len(gamePlayers)) - - log.Debug().Msg("inserting game player scores into db...") - batchGamePlayerScores := u.db.CreateOrUpdateGamePlayerScores( - ctx, - lo.Union( - lo.MapToSlice( - scores, - func(gameId string, scores []football.PlayerScore) []model.CreateOrUpdateGamePlayerScoresParams { - return lo.Map(scores, func(score football.PlayerScore, index int) model.CreateOrUpdateGamePlayerScoresParams { - return NewCreateOrUpdateGamePlayerScoresParamsFromSorare(gameId, score) - }) - - }, - )...), - ) - batcherr = nil - batchGamePlayerScores.Exec(func(i int, err error) { - if err != nil { - batcherr = err - batchGamePlayerScores.Close() - } - }) - if batcherr != nil { - return errors.Wrap(batcherr, "inserting game player scores") - } - log.Debug().Msgf("game player scores inserted") return nil } -func ExtractTeamSlugsFromPlayersAndGames(players []football.Player, games []football.Game) []string { - return lo.Uniq(lo.Union( - ExtractTeamSlugsFromPlayers(players), - ExtractTeamSlugsFromGames(games), - )) +func (u *UpdateService) SyncDatabaseForAllFixtures(ctx context.Context) error { + err := u.updateAllFixtures(ctx) + if err != nil { + return errors.Wrap(err, "updating fixtures") + } + fixtures, err := u.db.Fixtures.GetAll(ctx) + if err != nil { + return err + } + return u.SyncDatabaseForFixtures(ctx, fixtures) } -func ExtractTeamSlugsFromPlayers(players []football.Player) []string { - return lo.Uniq(lo.Filter(lo.Map(players, func(player football.Player, index int) string { - return player.ActiveClub.Slug - }), func(slug string, index int) bool { - return slug != "" - })) +func (u *UpdateService) UpdateLastClosedStartedAndOpenedFixtures(ctx context.Context) error { + err := u.updateAllFixtures(ctx) + if err != nil { + return errors.Wrap(err, "updating fixtures") + } + var fixtures []model.Fixture + lastClosedFixture, err := u.db.Fixtures.GetLastClosed(ctx) + if err != nil { + return err + } + fixtures = append(fixtures, lastClosedFixture) + openedFixtures, err := u.db.Fixtures.GetOpened(ctx) + if err != nil { + return err + } + fixtures = append(fixtures, openedFixtures...) + startedFixtures, err := u.db.Fixtures.GetStarted(ctx) + if err != nil { + return err + } + fixtures = append(fixtures, startedFixtures...) + return u.SyncDatabaseForFixtures(ctx, fixtures) } -func ExtractTeamSlugsFromGames(games []football.Game) []string { - var res []string - res = lo.Map(games, func(game football.Game, index int) string { - return game.AwayTeam.Team.Slug - }) - res = append(res, lo.Map(games, func(game football.Game, index int) string { - return game.HomeTeam.Team.Slug +func (u *UpdateService) GetGamePlayersForUpcomingGamesWithoutFormation(ctx context.Context) error { + log.Debug().Msg("updating formations for games in the next 3 hours") + gameIds, err := u.db.Games.GetFutureGameIdsWithoutFormation(ctx, 3*time.Hour, 1) + if err != nil { + return errors.Wrap(err, "getting future games") + } + log.Debug().Msgf("%d future games without formations found", len(gameIds)) + u.updater.Reset() + u.updater.AddGameFormationsToRead(gameIds...) + err = u.updater.Update(ctx) + if err != nil { + return errors.Wrap(err, "updating data") + } + return nil + +} + +func (u *UpdateService) UpdateCurrentlyPlayingGames(ctx context.Context) error { + games, err := u.db.Games.CurrentlyPlayingGames(ctx) + if err != nil { + return errors.Wrap(err, "getting currently playing games") + } + u.updater.Reset() + u.updater.AddGamesToRead(lo.Map(games, func(game model.Game, index int) string { + return game.Id })...) - res = lo.Filter(res, func(slug string, index int) bool { - return slug != "" - }) - return lo.Uniq(res) + err = u.updater.Update(ctx, + WithUpdateOnlyMissingPlayers(true), + WithUpdateOnlyMissingTeams(true), + WithUpdateOnlyMissingCompetitions(true), + WithUpdateOnlyMissingCountries(true), + ) + if err != nil { + return errors.Wrap(err, "updating data") + } + return nil } -func ExtractCountrySlugsFromCompetitions(competitions []football.Competition) []string { - return lo.Uniq(lo.Filter(lo.Map(competitions, func(competition football.Competition, index int) string { - return competition.Country.Slug - }), func(slug string, index int) bool { - return slug != "" - })) -} - -func ExtractCountrySlugsFromTeams(teams []football.Team) []string { - return lo.Uniq(lo.Filter(lo.Map(teams, func(team football.Team, index int) string { - return team.Country.Slug - }), func(slug string, index int) bool { - return slug != "" - })) -} - -func ExtractCountrySlugsFromPlayersCompetitionsClubsAndNationalTeams( - players []football.Player, - competitions []football.Competition, - clubs []football.Club, - nationalTeams []football.NationalTeam, -) []string { - return lo.Uniq(lo.Union( - ExtractCountrySlugsFromPlayers(players), - ExtractCountrySlugsFromCompetitions(competitions), - ExtractCountrySlugsFromTeams(lo.Map(clubs, func(club football.Club, index int) football.Team { - return club.Team - })), - ExtractCountrySlugsFromTeams( - lo.Map(nationalTeams, func(nationalTeam football.NationalTeam, index int) football.Team { - return nationalTeam.Team - }), - ), - )) -} - -func ExtractCountrySlugsFromPlayers(players []football.Player) []string { - return lo.Uniq(lo.Filter(lo.Map(players, func(player football.Player, index int) string { - return player.Country.Slug - }), func(slug string, index int) bool { - return slug != "" - })) - -} - -func ExtractCompetitionSlugsFromPlayers(players []football.Player) []string { - return lo.Uniq(lo.Filter(lo.Map(players, func(player football.Player, index int) string { - var res string - for _, competition := range player.ActiveClub.ActiveCompetitions { - if competition.Format == "DOMESTIC_LEAGUE" { - res = competition.Slug - } - } - return res - }), func(slug string, index int) bool { - return slug != "" - })) -} - -func ExtractCompetitionSlugsFromGames(games []football.Game) []string { - return lo.Uniq(lo.Filter(lo.Map(games, func(game football.Game, index int) string { - return game.Competition.Slug - }), func(slug string, index int) bool { - return slug != "" - })) -} - -func ExtractCompetitionSlugsFromClubs(clubs []football.Club) []string { - return lo.Uniq(lo.Filter(lo.Map(clubs, func(club football.Club, index int) string { - return club.DomesticLeague.Slug - }), func(slug string, index int) bool { - return slug != "" - })) -} - -func ExtractCompetitionSlugsFromPlayersGamesAndClubs( - players []football.Player, - games []football.Game, - clubs []football.Club, -) []string { - return lo.Uniq(lo.Union( - ExtractCompetitionSlugsFromPlayers(players), - ExtractCompetitionSlugsFromGames(games), - ExtractCompetitionSlugsFromClubs(clubs), - )) +func (u *UpdateService) LightUpdateStartedFixture(ctx context.Context) error { + fixture, err := u.db.Fixtures.GetStarted(ctx) + if err != nil { + return errors.Wrap(err, "getting started fixtures") + } + return u.SyncDatabaseForFixtures(ctx, fixture, + WithUpdateOnlyMissingCompetitions(true), + WithUpdateOnlyMissingCountries(true), + WithUpdateOnlyMissingPlayers(true), + WithUpdateOnlyMissingTeams(true)) } diff --git a/sorare_utils/updater.go b/sorare_utils/updater.go new file mode 100644 index 0000000..0bdd488 --- /dev/null +++ b/sorare_utils/updater.go @@ -0,0 +1,831 @@ +package sorare_utils + +import ( + "context" + "time" + + "git.lehouerou.net/laurent/sorare" + "git.lehouerou.net/laurent/sorare/football" + "git.lehouerou.net/laurent/sorare/graphql" + gql "github.com/llehouerou/go-graphql-client" + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + "github.com/samber/lo" + + "git.lehouerou.net/laurent/sorarebuddy/db" + "git.lehouerou.net/laurent/sorarebuddy/model" +) + +type Updater struct { + s *sorare.Sorare + db *db.Client + + countrySlugsToRead []string + competitionSlugsToRead []string + teamSlugsToRead []string + playerSlugsToRead []string + gamesFromFixtureSlugToRead []string + gamesToRead []string + gameFormationsToRead []string + gameScoresToReadFromMap map[string][]string + + countryCache []sorare.Country + competitionCache []football.Competition + clubCache []football.Club + nationalTeamCache []football.NationalTeam + playerCache []football.Player + gameCache []football.Game + gamePlayersCache []model.GamePlayer + gameScoreCache []model.GamePlayerScore +} + +type updaterOptions struct { + UpdateOnlyMissingCountries bool + UpdateOnlyMissingCompetitions bool + UpdateOnlyMissingTeams bool + UpdateOnlyMissingPlayers bool +} +type UpdaterOption func(*updaterOptions) *updaterOptions + +func WithUpdateOnlyMissingCountries(value bool) UpdaterOption { + return func(o *updaterOptions) *updaterOptions { + o.UpdateOnlyMissingCountries = value + return o + } +} + +func WithUpdateOnlyMissingCompetitions(value bool) UpdaterOption { + return func(o *updaterOptions) *updaterOptions { + o.UpdateOnlyMissingCompetitions = value + return o + } +} + +func WithUpdateOnlyMissingTeams(value bool) UpdaterOption { + return func(o *updaterOptions) *updaterOptions { + o.UpdateOnlyMissingTeams = value + return o + } +} + +func WithUpdateOnlyMissingPlayers(value bool) UpdaterOption { + return func(o *updaterOptions) *updaterOptions { + o.UpdateOnlyMissingPlayers = value + return o + } +} + +func NewUpdater(s *sorare.Sorare, db *db.Client, opts ...UpdaterOption) *Updater { + return &Updater{ + s: s, + db: db, + gameScoresToReadFromMap: make(map[string][]string), + } +} + +func (u *Updater) Reset() { + u.countrySlugsToRead = nil + u.competitionSlugsToRead = nil + u.teamSlugsToRead = nil + u.playerSlugsToRead = nil + u.gamesFromFixtureSlugToRead = nil + u.gamesToRead = nil + u.gameFormationsToRead = nil + for k := range u.gameScoresToReadFromMap { + delete(u.gameScoresToReadFromMap, k) + } + + u.countryCache = nil + u.competitionCache = nil + u.clubCache = nil + u.nationalTeamCache = nil + u.playerCache = nil + u.gameCache = nil + u.gamePlayersCache = nil + u.gameScoreCache = nil +} + +func (u *Updater) Update(ctx context.Context, opts ...UpdaterOption) error { + options := updaterOptions{ + UpdateOnlyMissingCountries: true, + UpdateOnlyMissingCompetitions: true, + UpdateOnlyMissingTeams: false, + UpdateOnlyMissingPlayers: false, + } + for _, opt := range opts { + options = *opt(&options) + } + + if err := u.readGamesFromFixture(ctx); err != nil { + return errors.Wrap(err, "reading games from fixture") + } + + if err := u.readGames(ctx); err != nil { + return errors.Wrap(err, "reading games") + } + + if err := u.readGamePlayers(ctx); err != nil { + return errors.Wrap(err, "reading game players") + } + + if err := u.readGameScoresFromMap(ctx); err != nil { + return errors.Wrap(err, "reading game scores") + } + + if err := u.readPlayers(ctx, options.UpdateOnlyMissingPlayers); err != nil { + return errors.Wrap(err, "reading players") + } + + if err := u.readTeams(ctx, options.UpdateOnlyMissingTeams); err != nil { + return errors.Wrap(err, "reading teams") + } + + if err := u.readCompetitions(ctx, options.UpdateOnlyMissingCompetitions); err != nil { + return errors.Wrap(err, "reading competitions") + } + + if err := u.readCountries(ctx, options.UpdateOnlyMissingCountries); err != nil { + return errors.Wrap(err, "reading countries") + } + + if err := u.writeCountries(ctx); err != nil { + return errors.Wrap(err, "writing countries") + } + + if err := u.writeCompetitions(ctx); err != nil { + return errors.Wrap(err, "writing competitions") + } + + if err := u.writeTeams(ctx); err != nil { + return errors.Wrap(err, "writing teams") + } + + if err := u.writePlayers(ctx); err != nil { + return errors.Wrap(err, "writing players") + } + + if err := u.writeGames(ctx); err != nil { + return errors.Wrap(err, "writing games") + } + + if err := u.writeGamePlayers(ctx); err != nil { + return errors.Wrap(err, "writing game players") + } + + if err := u.writeGameScores(ctx); err != nil { + return errors.Wrap(err, "writing game scores") + } + + return nil +} + +func (u *Updater) AddCountriesToRead(slugs ...string) { + slugs = lo.Filter(slugs, func(slug string, index int) bool { + return slug != "" + }) + u.countrySlugsToRead = lo.Uniq(append(u.countrySlugsToRead, slugs...)) +} + +func (u *Updater) readCountries(ctx context.Context, onlyMissings bool) error { + if len(u.countrySlugsToRead) == 0 { + log.Debug().Msg("no countries to read") + return nil + } + log.Debug().Msgf("reading %d countries...", len(u.countrySlugsToRead)) + slugs := u.countrySlugsToRead + u.countrySlugsToRead = nil + + if onlyMissings { + log.Debug().Msgf("filtering countries in db...") + slugsNotInDb, err := u.db.Countries.GetCountrySlugsNotInDb(ctx, slugs) + if err != nil { + return errors.Wrap(err, "getting countries not in db") + } + slugs = slugsNotInDb + log.Debug().Msgf("%d countries not in db", len(slugs)) + } + + log.Debug().Msgf("getting countries from sorare...") + u.countryCache = nil + for i, chunk := range lo.Chunk(slugs, 100) { + log.Debug().Msgf("\tbatch %d/%d", i+1, (len(slugs)/100)+1) + c, err := u.s.Countries.Get(ctx, graphql.SlugsParams{Slugs: chunk}) + if err != nil { + return err + } + u.countryCache = append(u.countryCache, c...) + } + log.Debug().Msgf("%d countries fetched from sorare", len(u.countryCache)) + return nil +} + +func (u *Updater) writeCountries(ctx context.Context) error { + log.Debug().Msg("inserting countries into db...") + err := u.db.Countries.CreateOrUpdateMany( + ctx, + lo.Map(u.countryCache, func(country sorare.Country, index int) model.Country { + return model.Country{ + Slug: country.Slug, + Code: country.Code, + DisplayName: country.Name, + ThreeLetterCode: country.ThreeLetterCode, + FlagFlat64Url: country.FlagFlat64Url, + FlagFlat32Url: country.FlagFlat32Url, + FlagRound64Url: country.FlagRound64Url, + FlagRound32Url: country.FlagRound32Url, + } + }), + ) + if err != nil { + return errors.Wrap(err, "inserting countries") + } + log.Debug().Msgf("%d countries inserted", len(u.countryCache)) + u.countryCache = nil + return nil +} + +func (u *Updater) AddCompetitionsToRead(slugs ...string) { + slugs = lo.Filter(slugs, func(slug string, index int) bool { + return slug != "" + }) + u.competitionSlugsToRead = lo.Uniq(append(u.competitionSlugsToRead, slugs...)) +} + +func (u *Updater) readCompetitions(ctx context.Context, onlyMissings bool) error { + if len(u.competitionSlugsToRead) == 0 { + log.Debug().Msg("no competitions to read") + return nil + } + log.Debug().Msgf("reading %d competitions...", len(u.competitionSlugsToRead)) + slugs := u.competitionSlugsToRead + u.competitionSlugsToRead = nil + + if onlyMissings { + log.Debug().Msgf("filtering competitions not in db...") + slugsNotInDb, err := u.db.Competitions.GetCompetitionSlugsNotInDb(ctx, slugs) + if err != nil { + return errors.Wrap(err, "getting competitions not in db") + } + slugs = slugsNotInDb + log.Debug().Msgf("%d competitions not in db", len(slugs)) + } + + log.Debug().Msgf("getting competitions...") + u.competitionCache = nil + for _, slug := range slugs { + log.Debug().Msgf("\tcompetition %s", slug) + c, err := u.s.Football.Competition.Get(ctx, graphql.SlugParams{Slug: slug}) + if err != nil { + return err + } + u.competitionCache = append(u.competitionCache, c) + } + log.Debug().Msgf("found %d competitions", len(u.competitionCache)) + + u.AddCountriesToRead(lo.Map(u.competitionCache, func(c football.Competition, index int) string { + return c.Country.Slug + })...) + + return nil +} + +func (u *Updater) writeCompetitions(ctx context.Context) error { + log.Debug().Msg("inserting competitions into db...") + + err := u.db.Competitions.CreateOrUpdateMany( + ctx, + lo.Map(u.competitionCache, func(competition football.Competition, index int) model.Competition { + return model.Competition{ + Slug: competition.Slug, + CompetitionFormat: competition.Format, + CompetitionType: competition.Type, + DisplayName: competition.DisplayName, + PictureUrl: competition.PictureUrl, + LogoUrl: competition.LogoUrl, + CountrySlug: competition.Country.Slug, + } + }), + ) + if err != nil { + return errors.Wrap(err, "inserting competitions") + } + log.Debug().Msgf("%d competitions inserted", len(u.competitionCache)) + u.competitionCache = nil + return nil +} + +func (u *Updater) AddTeamsToRead(slugs ...string) { + slugs = lo.Filter(slugs, func(slug string, index int) bool { + return slug != "" + }) + u.teamSlugsToRead = lo.Uniq(append(u.teamSlugsToRead, slugs...)) +} + +func (u *Updater) readTeams(ctx context.Context, onlyMissings bool) error { + if len(u.teamSlugsToRead) == 0 { + log.Debug().Msg("no teams to read") + return nil + } + log.Debug().Msgf("reading %d teams...", len(u.teamSlugsToRead)) + slugs := u.teamSlugsToRead + u.teamSlugsToRead = nil + + log.Debug().Msgf("getting clubs...") + if onlyMissings { + log.Debug().Msgf("filtering clubs not in db...") + slugsNotInDb, err := u.db.Teams.GetTeamSlugsNotInDb(ctx, slugs) + if err != nil { + return errors.Wrap(err, "getting teams not in db") + } + slugs = slugsNotInDb + log.Debug().Msgf("%d clubs not in db", len(slugs)) + } + + u.clubCache = nil + for i, chunk := range lo.Chunk(slugs, 100) { + log.Debug().Msgf("\tbatch %d/%d", i+1, (len(slugs)/100)+1) + t, err := u.s.Football.Clubs.Get(ctx, graphql.SlugsParams{Slugs: chunk}) + if err != nil { + return err + } + u.clubCache = append(u.clubCache, t...) + } + log.Debug().Msgf("found %d clubs", len(u.clubCache)) + slugsLeft := lo.Without(slugs, lo.Map(u.clubCache, func(club football.Club, index int) string { + return club.Slug + })...) + u.nationalTeamCache = nil + log.Debug().Msgf("getting national teams...") + log.Debug().Msgf("slugs left: %d", len(slugsLeft)) + for i, chunk := range lo.Chunk(slugsLeft, 100) { + log.Debug().Msgf("\tbatch %d/%d", i+1, (len(slugsLeft)/100)+1) + t, err := u.s.Football.NationalTeams.Get(ctx, graphql.SlugsParams{Slugs: chunk}) + if err != nil { + return err + } + u.nationalTeamCache = append(u.nationalTeamCache, t...) + } + log.Debug().Msgf("found %d national teams", len(u.nationalTeamCache)) + + u.AddCompetitionsToRead(lo.Map(u.clubCache, func(club football.Club, index int) string { + return club.DomesticLeague.Slug + })...) + + u.AddCountriesToRead(lo.Map(u.clubCache, func(club football.Club, index int) string { + return club.Country.Slug + })...) + + u.AddCountriesToRead(lo.Map(u.nationalTeamCache, func(nationalTeam football.NationalTeam, index int) string { + return nationalTeam.Country.Slug + })...) + + return nil +} + +func (u *Updater) writeTeams(ctx context.Context) error { + log.Debug().Msg("inserting teams into db...") + err := u.db.Teams.CreateOrUpdateMany(ctx, lo.Union( + lo.Map(u.clubCache, func(club football.Club, index int) model.Team { + return model.Team{ + Slug: club.Slug, + DisplayName: club.Name, + CountrySlug: club.Country.Slug, + DomesticLeagueSlug: func() *string { + if club.DomesticLeague.Slug == "" { + return nil + } + return &club.DomesticLeague.Slug + }(), + ShortName: club.ShortName, + PictureUrl: club.PictureUrl, + TeamType: "club", + } + }), + lo.Map(u.nationalTeamCache, func(nationalTeam football.NationalTeam, index int) model.Team { + return model.Team{ + Slug: nationalTeam.Slug, + DisplayName: nationalTeam.Name, + CountrySlug: nationalTeam.Country.Slug, + DomesticLeagueSlug: nil, + ShortName: nationalTeam.ShortName, + PictureUrl: nationalTeam.PictureUrl, + TeamType: "national", + } + }), + )) + if err != nil { + return errors.Wrap(err, "inserting teams") + } + log.Debug().Msgf("%d teams inserted", len(u.clubCache)+len(u.nationalTeamCache)) + u.clubCache = nil + u.nationalTeamCache = nil + return nil +} + +func (u *Updater) AddPlayersToRead(slugs ...string) { + slugs = lo.Filter(slugs, func(slug string, index int) bool { + return slug != "" + }) + u.playerSlugsToRead = lo.Uniq(append(u.playerSlugsToRead, slugs...)) +} + +func (u *Updater) readPlayers(ctx context.Context, onlyMissings bool) error { + if len(u.playerSlugsToRead) == 0 { + log.Debug().Msg("no players to read") + return nil + } + slugs := u.playerSlugsToRead + u.playerSlugsToRead = nil + + log.Debug().Msgf("updating %d players", len(slugs)) + + if onlyMissings { + log.Debug().Msgf("filtering players not in db...") + slugsNotInDb, err := u.db.Players.GetPlayerSlugsNotInDb(ctx, slugs) + if err != nil { + return errors.Wrap(err, "getting players not in db") + } + slugs = slugsNotInDb + log.Debug().Msgf("%d players not in db", len(slugs)) + } + + log.Debug().Msgf("getting players from sorare...") + u.playerCache = nil + for i, chunk := range lo.Chunk(slugs, 65) { + log.Debug().Msgf("\tbatch %d/%d", i+1, (len(slugs)/65)+1) + p, err := u.s.Football.Players.Get(ctx, graphql.SlugsParams{Slugs: chunk}) + if err != nil { + return errors.Wrapf(err, "getting players batch %d", i) + } + u.playerCache = append(u.playerCache, p...) + } + log.Debug().Msgf("found %d players", len(u.playerCache)) + + u.AddTeamsToRead(lo.FlatMap(u.playerCache, func(player football.Player, index int) []string { + var res []string + res = append(res, player.ActiveClub.Slug) + res = append(res, player.ActiveNationalTeam.Slug) + res = append(res, lo.Map(player.Memberships, func(membership football.Membership, index int) string { + if membership.MembershipTeam.Club.Slug != "" { + return membership.MembershipTeam.Club.Slug + } else if membership.MembershipTeam.NationalTeam.Slug != "" { + return membership.MembershipTeam.NationalTeam.Slug + } + return "" + })...) + return res + })...) + + u.AddCompetitionsToRead(lo.Map(u.playerCache, func(player football.Player, index int) string { + var res string + for _, competition := range player.ActiveClub.ActiveCompetitions { + if competition.Format == "DOMESTIC_LEAGUE" { + res = competition.Slug + } + } + return res + })...) + + u.AddCountriesToRead(lo.Map(u.playerCache, func(player football.Player, index int) string { + return player.Country.Slug + })...) + + return nil +} + +func (u *Updater) writePlayers(ctx context.Context) error { + log.Debug().Msg("inserting players into db...") + err := u.db.Players.CreateOrUpdateMany( + ctx, + lo.Map(u.playerCache, func(player football.Player, index int) model.Player { + res := model.Player{ + Slug: player.Slug, + DisplayName: player.DisplayName, + BirthDate: player.BirthDate, + CountrySlug: player.Country.Slug, + AvatarUrl: player.AvatarUrl, + FieldPosition: string(player.Position), + Status: string(player.PlayingStatus), + ShirtNumber: int(player.ShirtNumber), + ActiveNationalTeamSlug: func() *string { + if player.ActiveNationalTeam.Slug == "" { + return nil + } + return &player.ActiveNationalTeam.Slug + }(), + } + for _, competition := range player.ActiveClub.ActiveCompetitions { + if competition.Format == "DOMESTIC_LEAGUE" { + res.DomesticLeagueSlug = &competition.Slug + } + } + if player.ActiveClub.Slug != "" { + res.TeamSlug = &player.ActiveClub.Slug + } + return res + }), + ) + if err != nil { + return errors.Wrap(err, "inserting players") + } + log.Debug().Msgf("%d players inserted", len(u.playerCache)) + + log.Debug().Msgf("inserting players card supply into db...") + err = u.db.CardSupplies.CreateOrUpdateMany( + ctx, + lo.FlatMap(u.playerCache, func(player football.Player, index int) []model.CardSupply { + var res []model.CardSupply + for _, supply := range player.CardSupply { + res = append(res, model.CardSupply{ + PlayerSlug: player.Slug, + SeasonStartYear: supply.Season.StartYear, + Limited: supply.Limited, + Rare: supply.Rare, + SuperRare: supply.SuperRare, + Unique: supply.Unique, + LastUpdated: time.Now(), + }) + } + return res + }), + ) + if err != nil { + return errors.Wrap(err, "inserting players card supply") + } + log.Debug().Msgf("%d players card supply inserted", len(u.playerCache)) + + log.Debug().Msgf("inserting players club_memberships into db...") + err = u.db.Memberships.CreateOrUpdateMany( + ctx, + lo.FlatMap(u.playerCache, func(player football.Player, index int) []model.Membership { + var res []model.Membership + for _, membership := range player.Memberships { + new := model.Membership{ + Id: membership.Id.Value, + PlayerSlug: player.Slug, + StartDate: membership.StartDate, + EndDate: membership.EndDate, + } + if membership.MembershipTeam.TypeName == "Club" { + new.TeamSlug = membership.MembershipTeam.Club.Slug + new.MembershipType = "club" + } else if membership.MembershipTeam.TypeName == "NationalTeam" { + new.TeamSlug = membership.MembershipTeam.NationalTeam.Slug + new.MembershipType = "national" + } else { + continue + } + res = append(res, new) + } + return res + }), + ) + if err != nil { + return errors.Wrap(err, "inserting players club_memberships") + } + log.Debug().Msgf("%d players club_memberships inserted", len(u.playerCache)) + + u.playerCache = nil + return nil +} + +func (u *Updater) AddGamesFromFixtureToRead(slugs ...string) { + slugs = lo.Filter(slugs, func(slug string, index int) bool { + return slug != "" + }) + u.gamesFromFixtureSlugToRead = lo.Uniq(append(u.gamesFromFixtureSlugToRead, slugs...)) +} + +func (u *Updater) readGamesFromFixture(ctx context.Context) error { + if len(u.gamesFromFixtureSlugToRead) == 0 { + log.Debug().Msg("no games from fixture to read") + return nil + } + slugs := u.gamesFromFixtureSlugToRead + u.gamesFromFixtureSlugToRead = nil + + log.Debug().Msgf("updating games for fixtures %v", slugs) + u.gameCache = nil + for _, slug := range slugs { + log.Debug().Msgf("getting games for fixture %s...", slug) + g, err := u.s.Football.So5.FixtureGames(slug).Get(ctx, graphql.EmptyParams{}) + if err != nil { + return errors.Wrapf(err, "getting games for fixture %s", slug) + } + g = lo.Filter(g, func(game football.Game, index int) bool { + return game.Id.Value != "" + }) + log.Debug().Msgf("found %d games", len(g)) + u.gameCache = append(u.gameCache, g...) + } + u.gameCache = lo.Filter(u.gameCache, func(game football.Game, index int) bool { + return game.So5Fixture.Slug != "" + }) + u.gameCache = lo.UniqBy(u.gameCache, func(game football.Game) string { + return game.Id.Value + }) + u.AddTeamsToRead(lo.Union( + lo.Map(u.gameCache, func(game football.Game, index int) string { + return game.AwayTeam.Team.Slug + }), + lo.Map(u.gameCache, func(game football.Game, index int) string { + return game.HomeTeam.Team.Slug + }), + )...) + u.AddCompetitionsToRead(lo.Map(u.gameCache, func(game football.Game, index int) string { + return game.Competition.Slug + })...) + u.AddGameFormationsToRead(lo.Map(u.gameCache, func(game football.Game, index int) string { + return game.Id.Value + })...) + return nil +} + +func (u *Updater) writeGames(ctx context.Context) error { + log.Debug().Msg("inserting games into db...") + err := u.db.Games.CreateOrUpdateMany( + ctx, + lo.Map(u.gameCache, func(game football.Game, index int) model.Game { + return NewGameFromSorare(game) + }), + ) + if err != nil { + return errors.Wrap(err, "inserting games") + } + log.Debug().Msgf("%d games inserted", len(u.gameCache)) + u.gameCache = nil + return nil +} + +func (u *Updater) AddGameFormationsToRead(ids ...string) { + ids = lo.Filter(ids, func(id string, index int) bool { + return id != "" + }) + u.gameFormationsToRead = lo.Uniq(append(u.gameFormationsToRead, ids...)) +} + +func (u *Updater) readGamePlayers(ctx context.Context) error { + if len(u.gameFormationsToRead) == 0 { + log.Debug().Msg("no game formations to read") + return nil + } + ids := u.gameFormationsToRead + u.gameFormationsToRead = nil + playerSlugsByGameMap := make(map[string][]string) + for _, chunk := range lo.Chunk(ids, 50) { + gamesWithFormation, err := u.s.Football.GamesFormation.Get(ctx, chunk) + if err != nil { + return errors.Wrapf(err, "getting games with formation for games %v", ids) + } + for _, game := range gamesWithFormation { + newplayers := model.ExtractPlayersFromGameWithFormation(game) + log.Debug().Msgf("\t%s -> %d players", game.Id.Value, len(newplayers)) + playerSlugsByGameMap[game.Id.Value] = lo.Map( + newplayers, + func(player model.GamePlayer, index int) string { + return player.PlayerSlug + }, + ) + u.gamePlayersCache = append(u.gamePlayersCache, newplayers...) + } + } + u.gamePlayersCache = lo.UniqBy(u.gamePlayersCache, func(player model.GamePlayer) string { + return player.GameId + "-" + player.PlayerSlug + }) + u.AddPlayersToRead(lo.Map(u.gamePlayersCache, func(player model.GamePlayer, index int) string { + return player.PlayerSlug + })...) + u.AddGameScoresFromMapToRead(playerSlugsByGameMap) + return nil +} + +func (u *Updater) writeGamePlayers(ctx context.Context) error { + log.Debug().Msg("inserting game players into db...") + err := u.db.GamePlayers.CreateOrUpdateMany( + ctx, + u.gamePlayersCache, + ) + if err != nil { + return errors.Wrap(err, "inserting game players") + } + log.Debug().Msgf("%d game players inserted", len(u.gamePlayersCache)) + u.gamePlayersCache = nil + return nil +} + +func (u *Updater) AddGameScoresFromMapToRead(gameScores map[string][]string) { + for gameId, playerSlugs := range gameScores { + u.gameScoresToReadFromMap[gameId] = append(u.gameScoresToReadFromMap[gameId], playerSlugs...) + } + for gameId, playerSlugs := range u.gameScoresToReadFromMap { + u.gameScoresToReadFromMap[gameId] = lo.Uniq(playerSlugs) + } +} + +func (u *Updater) readGameScoresFromMap(ctx context.Context) error { + if len(u.gameScoresToReadFromMap) == 0 { + log.Debug().Msg("no game scores to read") + return nil + } + for gameId, playerSlugs := range u.gameScoresToReadFromMap { + if len(playerSlugs) == 0 { + delete(u.gameScoresToReadFromMap, gameId) + } + } + + gameIdList := lo.MapToSlice(u.gameScoresToReadFromMap, func(key string, value []string) string { + return key + }) + + for _, gameIds := range lo.Chunk(gameIdList, 2) { + log.Debug().Msgf("getting scores for games %v", gameIds) + params := make(map[string][]string) + for _, gameId := range gameIds { + if u.gameScoresToReadFromMap[gameId] != nil && len(u.gameScoresToReadFromMap[gameId]) > 0 { + params[gameId] = u.gameScoresToReadFromMap[gameId] + } + } + if len(params) == 0 { + continue + } + scores, err := u.s.Football.GamesScores.Get(ctx, params) + if err != nil { + return errors.Wrap(err, "getting scores") + } + scores = lo.Filter(scores, func(score football.So5Score, index int) bool { + return score.Player.Slug != "" && score.Game.Id.Value != "" + }) + u.gameScoreCache = append(u.gameScoreCache, lo.Map( + scores, + func(score football.So5Score, index int) model.GamePlayerScore { + return NewGamePlayerScoreFromSorare(score.Game.Id.Value, score) + }, + )...) + } + for k := range u.gameScoresToReadFromMap { + delete(u.gameScoresToReadFromMap, k) + } + + return nil +} + +func (u *Updater) writeGameScores(ctx context.Context) error { + log.Debug().Msg("inserting game scores from map into db...") + err := u.db.GamePlayerScores.CreateOrUpdateMany( + ctx, + u.gameScoreCache, + ) + if err != nil { + return errors.Wrap(err, "inserting game scores from map") + } + log.Debug().Msgf("%d game scores from map inserted", len(u.gameScoreCache)) + u.gameScoreCache = nil + return nil +} + +func (u *Updater) AddGamesToRead(ids ...string) { + ids = lo.Filter(ids, func(id string, index int) bool { + return id != "" + }) + u.gamesToRead = lo.Uniq(append(u.gamesToRead, ids...)) +} + +func (u *Updater) readGames(ctx context.Context) error { + if len(u.gamesToRead) == 0 { + log.Debug().Msg("no games to read") + return nil + } + ids := u.gamesToRead + u.gamesToRead = nil + + log.Debug().Msgf("updating games %v", ids) + u.gameCache = nil + for _, id := range ids { + g, err := u.s.Football.Game.Get(ctx, graphql.IdParams{Id: gql.ID(id)}) + if err != nil { + return errors.Wrapf(err, "getting game %s", id) + } + u.gameCache = append(u.gameCache, g.Game) + } + u.gameCache = lo.Filter(u.gameCache, func(game football.Game, index int) bool { + return game.So5Fixture.Slug != "" + }) + u.gameCache = lo.UniqBy(u.gameCache, func(game football.Game) string { + return game.Id.Value + }) + u.AddTeamsToRead(lo.Union( + lo.Map(u.gameCache, func(game football.Game, index int) string { + return game.AwayTeam.Team.Slug + }), + lo.Map(u.gameCache, func(game football.Game, index int) string { + return game.HomeTeam.Team.Slug + }), + )...) + u.AddCompetitionsToRead(lo.Map(u.gameCache, func(game football.Game, index int) string { + return game.Competition.Slug + })...) + u.AddGameFormationsToRead(lo.Map(u.gameCache, func(game football.Game, index int) string { + return game.Id.Value + })...) + return nil +} diff --git a/sqlc.yml b/sqlc.yml deleted file mode 100644 index 9323e18..0000000 --- a/sqlc.yml +++ /dev/null @@ -1,28 +0,0 @@ -version: "2" -sql: - - engine: "postgresql" - queries: "model/sql/*.sql" - schema: "db/migrations" - database: - uri: postgresql://sorare:sorare@192.168.1.250:5436/sorare?sslmode=disable - gen: - go: - package: "model" - out: "model" - sql_package: "pgx/v5" - emit_pointers_for_null_types: true - overrides: - - db_type: "timestamptz" - go_type: "time.Time" - - db_type: "date" - go_type: "time.Time" - - db_type: "pg_catalog.numeric" - go_type: - import: "github.com/shopspring/decimal" - type: "Decimal" - - db_type: "numeric" - go_type: - import: "github.com/shopspring/decimal" - type: "Decimal" - -