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 0000000..f2eb4d4 Binary files /dev/null and b/front/bun.lockb differ diff --git a/front/index.html b/front/index.html new file mode 100644 index 0000000..b47a026 --- /dev/null +++ b/front/index.html @@ -0,0 +1,13 @@ + + + + + + + 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 0000000..9414790 Binary files /dev/null and b/front/public/favicon.ico differ 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" - -