diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 72ef0ee..d39f022 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,6 +1,6 @@ name: Deploy application with Docker -on: +on: push: branches: - master @@ -19,10 +19,22 @@ jobs: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_TOKEN }} - - name: Build and push Docker image + - name: Build and push backend Docker image uses: docker/build-push-action@v3 with: context: . file: ./Dockerfile push: true tags: azpect3120/potion.gophernest:latest + + - name: Build and push frontend Docker image + uses: docker/build-push-action@v5 + with: + context: ./web + file: ./Dockerfile + push: true + tags: azpect3120/potion.frontend:latest + build-args: | + VITE_ENVIRONMENT=prod + VITE_DOMAIN_DEV=http://localhost:3000 + VITE_DOMAIN_PROD=https://potion-backend.gophernest.net diff --git a/Dockerfile b/Dockerfile index 3e3b3f1..cbf0426 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,57 +1,14 @@ -# Fetch stage -FROM golang:latest AS fetch-stage - -COPY . /app +FROM golang:1.25-alpine WORKDIR /app -RUN go mod tidy +COPY go.mod go.sum ./ RUN go mod download -# Generate stage -FROM ghcr.io/a-h/templ:latest AS generate-stage +COPY . . -COPY --chown=65532:65532 . /app +RUN go build -o server ./cmd/web/main.go -WORKDIR /app +EXPOSE 8080 -RUN ["templ", "generate"] - -# Generate stage two - -FROM node:lts-alpine AS tailwind-build-stage - -COPY --from=generate-stage /app /app - -WORKDIR /app - -RUN npm install tailwindcss @tailwindcss/cli && npx @tailwindcss/cli -i ./web/static/css/main.css -o ./web/static/css/tailwind.css --minify -c ./tailwind.config.js - - -# Build stage -FROM golang:latest AS build-stage - -COPY --from=tailwind-build-stage /app /app - -WORKDIR /app - -RUN go mod tidy -RUN go mod download - -RUN CGO_ENABLED=0 GOOS=linux go build -o /entrypoint /app/cmd/web/main.go - -# Deploy. -FROM gcr.io/distroless/static-debian11 AS release-stage - -WORKDIR / - -COPY --from=build-stage /entrypoint /entrypoint - -COPY --from=build-stage /app/web/static /web/static - - -EXPOSE 3000 - -USER nonroot:nonroot - -ENTRYPOINT ["/entrypoint"] +CMD ["./server"] diff --git a/Dockerfile.old b/Dockerfile.old new file mode 100644 index 0000000..3e3b3f1 --- /dev/null +++ b/Dockerfile.old @@ -0,0 +1,57 @@ +# Fetch stage +FROM golang:latest AS fetch-stage + +COPY . /app + +WORKDIR /app + +RUN go mod tidy +RUN go mod download + +# Generate stage +FROM ghcr.io/a-h/templ:latest AS generate-stage + +COPY --chown=65532:65532 . /app + +WORKDIR /app + +RUN ["templ", "generate"] + +# Generate stage two + +FROM node:lts-alpine AS tailwind-build-stage + +COPY --from=generate-stage /app /app + +WORKDIR /app + +RUN npm install tailwindcss @tailwindcss/cli && npx @tailwindcss/cli -i ./web/static/css/main.css -o ./web/static/css/tailwind.css --minify -c ./tailwind.config.js + + +# Build stage +FROM golang:latest AS build-stage + +COPY --from=tailwind-build-stage /app /app + +WORKDIR /app + +RUN go mod tidy +RUN go mod download + +RUN CGO_ENABLED=0 GOOS=linux go build -o /entrypoint /app/cmd/web/main.go + +# Deploy. +FROM gcr.io/distroless/static-debian11 AS release-stage + +WORKDIR / + +COPY --from=build-stage /entrypoint /entrypoint + +COPY --from=build-stage /app/web/static /web/static + + +EXPOSE 3000 + +USER nonroot:nonroot + +ENTRYPOINT ["/entrypoint"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..026ee38 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,6 @@ +services: + app: + build: ./web/. + container_name: potion.frontend + ports: + - "3002:3002" # host:container diff --git a/flake.nix b/flake.nix index 6efaf29..2465d44 100644 --- a/flake.nix +++ b/flake.nix @@ -29,6 +29,7 @@ dockerfile-language-server-nodejs gcc_multi glibc_multi + nodejs ]; # Define the shell that will be executed. diff --git a/internal/app/handlers/auth_handler.go b/internal/app/handlers/auth_handler.go deleted file mode 100644 index 50d48df..0000000 --- a/internal/app/handlers/auth_handler.go +++ /dev/null @@ -1,47 +0,0 @@ -package handlers - -// GoogleLogin directs the user to Googles select user login page. Once the user has selected an -// account, they will be directed to the GoogleCallback handler where the main logic resides. -// -// DEPRECATED: As of September 4th, 2025. -// func GoogleLogin(ctx *gin.Context) { -// deps := ctx.MustGet("deps").(*domain.InjectedDependencies) -// url := deps.AuthService.GetGoogleAuthUrl() -// -// ctx.Redirect(http.StatusSeeOther, url) -// } - -// GoogleCallback is the callback handler when the user successfully logs in with their Google -// account. They will be directed here and a JWT is generated. This JWT is stored in the users -// cookies and will be used by protected routes to validate their login status. -// -// We do not need to return all of this data, it is just for testing. -// -// DEPRECATED: As of September 4th, 2025. -// func GoogleCallback(ctx *gin.Context) { -// deps := ctx.MustGet("deps").(*domain.InjectedDependencies) -// -// var ( -// state string = ctx.Query("state") -// code string = ctx.Query("code") -// ) -// -// if jwt, err := deps.AuthService.GoogleAuthSuccess(state, code); err != nil { -// components.RenderErrorBanner(ctx, err.Error()) -// ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) -// } else { -// domain.SetCookie(ctx, "jwt_token", jwt, time.Hour*24*7) -// ctx.Redirect(http.StatusSeeOther, "/") -// } -// } - -// Logout removes the token from the user's browser. Effectively "logging them out." Routes that -// require authentication will require the user to sign back in before accessing them again. -// This route will direct the user back to the home page. -// -// DEPRECATED: As of September 4th, 2025. -// func Logout(ctx *gin.Context) { -// domain.SetCookie(ctx, "jwt_token", "", -1) -// domain.SetCookie(ctx, "search-filters", "", -1) -// ctx.Redirect(http.StatusSeeOther, domain.WEB_HOME) -// } diff --git a/internal/app/handlers/engagement_handler.go b/internal/app/handlers/engagement_handler.go deleted file mode 100644 index 3620d04..0000000 --- a/internal/app/handlers/engagement_handler.go +++ /dev/null @@ -1,142 +0,0 @@ -package handlers - -// DEPRECATED: As of September 4th, 2025. -// func EngagementViewRecipe(ctx *gin.Context) { -// deps := ctx.MustGet("deps").(*domain.InjectedDependencies) -// recipeId, _ := strconv.Atoi(ctx.Param("id")) -// -// // Ensure user is logged in with a valid account -// user := deps.UserService.GetAuthenicatedUser(ctx) -// if user == nil { -// // Log (stale) user out -// domain.SetCookie(ctx, "jwt_token", "", -1) -// domain.SetCookie(ctx, "search-filters", "", -1) -// } -// -// if !domain.IsLoggedIn(ctx) || user == nil { -// if _, err := deps.EngagementService.ViewRecipe(recipeId); err != nil { -// components.RenderErrorBanner(ctx, err.Error()) -// ctx.JSON(http.StatusInternalServerError, gin.H{ -// "status": http.StatusInternalServerError, -// "message": err.Error(), -// }) -// } else { -// ctx.Header("HX-Redirect", fmt.Sprintf(domain.WEB_RECIPE, recipeId)) -// ctx.Status(http.StatusOK) -// } -// return -// } -// -// // We caught nil already, we can assume the user exists -// if _, err := deps.EngagementService.UserViewRecipe(user.Id, recipeId); err != nil { -// components.RenderErrorBanner(ctx, err.Error()) -// ctx.JSON(http.StatusInternalServerError, gin.H{ -// "status": http.StatusInternalServerError, -// "message": err.Error(), -// }) -// } else { -// ctx.Header("HX-Redirect", fmt.Sprintf(domain.WEB_RECIPE, recipeId)) -// ctx.Status(http.StatusOK) -// } -// } - -// DEPRECATED: As of September 4th, 2025. -// func EngagementShareRecipe(ctx *gin.Context) { -// deps := ctx.MustGet("deps").(*domain.InjectedDependencies) -// recipeId, _ := strconv.Atoi(ctx.Param("id")) -// -// // Ensure user is logged in with a valid account -// user := deps.UserService.GetAuthenicatedUser(ctx) -// if user == nil { -// // Log (stale) user out -// domain.SetCookie(ctx, "jwt_token", "", -1) -// domain.SetCookie(ctx, "search-filters", "", -1) -// } -// -// if !domain.IsLoggedIn(ctx) || user == nil { -// if _, err := deps.EngagementService.ShareRecipe(recipeId); err != nil { -// components.RenderErrorBanner(ctx, err.Error()) -// ctx.JSON(http.StatusInternalServerError, gin.H{ -// "status": http.StatusInternalServerError, -// "message": err.Error(), -// }) -// } else { -// ctx.Status(http.StatusNoContent) -// } -// return -// } -// -// if _, err := deps.EngagementService.UserShareRecipe(user.Id, recipeId); err != nil { -// components.RenderErrorBanner(ctx, err.Error()) -// ctx.JSON(http.StatusInternalServerError, gin.H{ -// "status": http.StatusInternalServerError, -// "message": err.Error(), -// }) -// } else { -// ctx.Status(http.StatusNoContent) -// } -// } - -// DEPRECATED: As of September 4th, 2025. -// func EngagementFavoriteRecipe(ctx *gin.Context) { -// deps := ctx.MustGet("deps").(*domain.InjectedDependencies) -// -// // Ensure user is logged in with a valid account -// user := deps.UserService.GetAuthenicatedUser(ctx) -// if user == nil { -// // Log (stale) user out -// domain.SetCookie(ctx, "jwt_token", "", -1) -// domain.SetCookie(ctx, "search-filters", "", -1) -// } -// -// if !domain.IsLoggedIn(ctx) || user == nil { -// ctx.Header("HX-Redirect", domain.WEB_LOGIN) -// ctx.Status(http.StatusOK) -// return -// } -// -// id := ctx.Param("id") -// recipeId, _ := strconv.Atoi(id) -// -// if _, err := deps.EngagementService.UserFavoriteRecipe(user.Id, recipeId); err != nil { -// components.RenderErrorBanner(ctx, fmt.Sprintf("Something went wrong. %s.", err.Error())) -// ctx.JSON(http.StatusInternalServerError, gin.H{ -// "status": http.StatusInternalServerError, -// "message": err.Error(), -// }) -// } else { -// ctx.Status(http.StatusNoContent) -// } -// } - -// DEPRECATED: As of September 4th, 2025. -// func EngagementMakeRecipe(ctx *gin.Context) { -// deps := ctx.MustGet("deps").(*domain.InjectedDependencies) -// -// // Ensure user is logged in with a valid account -// user := deps.UserService.GetAuthenicatedUser(ctx) -// if user == nil { -// // Log (stale) user out -// domain.SetCookie(ctx, "jwt_token", "", -1) -// domain.SetCookie(ctx, "search-filters", "", -1) -// } -// -// if !domain.IsLoggedIn(ctx) || user == nil { -// ctx.Header("HX-Redirect", domain.WEB_LOGIN) -// ctx.Status(http.StatusOK) -// return -// } -// -// id := ctx.Param("id") -// recipeId, _ := strconv.Atoi(id) -// -// if _, err := deps.EngagementService.UserMakeRecipe(user.Id, recipeId); err != nil { -// components.RenderErrorBanner(ctx, err.Error()) -// ctx.JSON(http.StatusInternalServerError, gin.H{ -// "status": http.StatusInternalServerError, -// "message": err.Error(), -// }) -// } else { -// ctx.Status(http.StatusNoContent) -// } -// } diff --git a/internal/app/handlers/page_handler.go b/internal/app/handlers/page_handler.go deleted file mode 100755 index 1165ecd..0000000 --- a/internal/app/handlers/page_handler.go +++ /dev/null @@ -1,296 +0,0 @@ -package handlers - -// DEPRECATED: As of September 4th, 2025. -// func LoginPage(ctx *gin.Context) { -// title := "Potion - Login" -// page := pages.LoginPage() -// -// ctx.HTML(http.StatusOK, "", layouts.AppLayout(title, page)) -// } - -// DEPRECATED: As of September 4th, 2025. -// func HomePage(ctx *gin.Context) { -// deps := ctx.MustGet("deps").(*domainServer.InjectedDependencies) -// -// loggedIn := domain.IsLoggedIn(ctx) -// -// // Ensure user is logged in with a valid account -// if user := deps.UserService.GetAuthenicatedUser(ctx); user == nil { -// // Log (stale) user out -// domain.SetCookie(ctx, "jwt_token", "", -1) -// domain.SetCookie(ctx, "search-filters", "", -1) -// loggedIn = false -// } -// -// var page templ.Component -// if loggedIn { -// userId := ctx.MustGet("userId").(int) -// madeRecipes, err := deps.RecipeService.GetUserMadeRecipes(userId, 6) -// if err != nil { -// components.RenderErrorBanner(ctx, fmt.Sprintf("Error getting made recipes. %s\n", err.Error())) -// ctx.JSON(http.StatusInternalServerError, gin.H{ -// "status": http.StatusInternalServerError, -// "message": fmt.Sprintf("Error getting made recipes. %s\n", err.Error()), -// }) -// return -// } -// viewedRecipes, err := deps.RecipeService.GetUserViewedRecipes(userId, 6) -// if err != nil { -// components.RenderErrorBanner(ctx, fmt.Sprintf("Error getting viewed recipes. %s\n", err.Error())) -// ctx.JSON(http.StatusInternalServerError, gin.H{ -// "status": http.StatusInternalServerError, -// "message": fmt.Sprintf("Error getting viewed recipes. %s\n", err.Error()), -// }) -// return -// } -// -// // Get the recipe of the week -// recipeOfTheWeek, err := deps.RecipeService.GetRecipeOfTheWeek(&userId) -// if err != nil { -// components.RenderErrorBanner(ctx, fmt.Sprintf("Error getting recipe of the week. %s\n", err.Error())) -// ctx.JSON(http.StatusInternalServerError, gin.H{ -// "status": http.StatusInternalServerError, -// "message": fmt.Sprintf("Error getting recipe of the week. %s\n", err.Error()), -// }) -// return -// } -// -// if bytes, err := ctx.Cookie("search-filters"); err != nil { -// fmt.Printf("ERROR: Failed to get search-filter cookie. %s\n", err.Error()) -// page = templates.HomePage(true, viewedRecipes, madeRecipes, recipeOfTheWeek, nil) -// } else { -// var filters domainRecipe.SearchFilters -// if err := json.Unmarshal([]byte(bytes), &filters); err != nil { -// fmt.Printf("ERROR: Failed to unmarshal search-filter cookie. %s\n", err.Error()) -// page = templates.HomePage(true, viewedRecipes, madeRecipes, recipeOfTheWeek, nil) -// } else { -// page = templates.HomePage(true, viewedRecipes, madeRecipes, recipeOfTheWeek, &filters) -// } -// } -// } else { -// // Get the recipe of the week -// recipeOfTheWeek, err := deps.RecipeService.GetRecipeOfTheWeek(nil) -// if err != nil { -// components.RenderErrorBanner(ctx, fmt.Sprintf("Error getting recipe of the week. %s\n", err.Error())) -// ctx.JSON(http.StatusInternalServerError, gin.H{ -// "status": http.StatusInternalServerError, -// "message": fmt.Sprintf("Error getting recipe of the week. %s\n", err.Error()), -// }) -// return -// } -// -// if bytes, err := ctx.Cookie("search-filters"); err != nil { -// fmt.Printf("ERROR: Failed to get search-filter cookie. %s\n", err.Error()) -// page = templates.HomePage(false, nil, nil, recipeOfTheWeek, nil) -// } else { -// var filters domainRecipe.SearchFilters -// if err := json.Unmarshal([]byte(bytes), &filters); err != nil { -// fmt.Printf("ERROR: Failed to unmarshal search-filter cookie. %s\n", err.Error()) -// page = templates.HomePage(false, nil, nil, recipeOfTheWeek, nil) -// } else { -// page = templates.HomePage(false, nil, nil, recipeOfTheWeek, &filters) -// } -// } -// } -// -// title := "Potion - Home" -// -// ctx.HTML(http.StatusOK, "", layouts.AppLayout(title, page)) -// } - -// DEPRECATED: As of September 4th, 2025. -// func FavoritesPage(ctx *gin.Context) { -// // If not logged in, direct to the login page -// if !domainServer.IsLoggedIn(ctx) { -// ctx.Redirect(http.StatusSeeOther, domainServer.WEB_LOGIN) -// return -// } -// -// title := "Potion - Favorites" -// var page templ.Component -// -// // Get filters from cookies -// if bytes, err := ctx.Cookie("search-filters"); err != nil { -// fmt.Printf("ERROR: Failed to get search-filter cookie. %s\n", err.Error()) -// page = pages.FavoritesPage(nil) -// } else { -// var filters domainRecipe.SearchFilters -// if err := json.Unmarshal([]byte(bytes), &filters); err != nil { -// fmt.Printf("ERROR: Failed to unmarshal search-filter cookie. %s\n", err.Error()) -// page = pages.FavoritesPage(nil) -// } else { -// page = pages.FavoritesPage(&filters) -// } -// } -// -// ctx.HTML(http.StatusOK, "", layouts.AppLayout(title, page)) -// } -// -// func CreatePage(ctx *gin.Context) { -// deps := ctx.MustGet("deps").(*domainServer.InjectedDependencies) -// -// // If not logged in, direct to the login page -// if !domainServer.IsLoggedIn(ctx) { -// ctx.Redirect(http.StatusSeeOther, domainServer.WEB_LOGIN) -// return -// } -// -// // Ensure user is logged in with a valid account -// if user := deps.UserService.GetAuthenicatedUser(ctx); user == nil { -// // Log (stale) user out -// domain.SetCookie(ctx, "jwt_token", "", -1) -// domain.SetCookie(ctx, "search-filters", "", -1) -// -// ctx.Redirect(http.StatusSeeOther, domainServer.WEB_LOGIN) -// return -// } -// -// title := "Potion - Create" -// page := pages.CreatePage() -// -// ctx.HTML(http.StatusOK, "", layouts.AppLayout(title, page)) -// } - -// DEPRECATED: As of September 4th, 2025. -// func ProfilePage(ctx *gin.Context) { -// // If not logged in, direct to the login page -// if !domainServer.IsLoggedIn(ctx) { -// ctx.Redirect(http.StatusSeeOther, domainServer.WEB_LOGIN) -// return -// } -// -// // Else, get the user data -// deps := ctx.MustGet("deps").(*domainServer.InjectedDependencies) -// user := deps.UserService.GetAuthenicatedUser(ctx) -// if user == nil { -// // User is failing to be found, direct to the login page -// ctx.Redirect(http.StatusSeeOther, domainServer.WEB_LOGIN) -// return -// } -// -// recipes, err := deps.RecipeService.GetUserRecipes(user.Id) -// if err != nil { -// components.RenderErrorBanner(ctx, fmt.Sprintf("Error getting recipes. %s\n", err.Error())) -// ctx.JSON(http.StatusInternalServerError, gin.H{ -// "status": http.StatusInternalServerError, -// "message": fmt.Sprintf("Error getting recipes. %s\n", err.Error()), -// }) -// return -// } -// -// favorites, err := deps.RecipeService.GetUserFavoriteRecipes(user.Id) -// if err != nil { -// components.RenderErrorBanner(ctx, fmt.Sprintf("Error getting favorite recipes. %s\n", err.Error())) -// ctx.JSON(http.StatusInternalServerError, gin.H{ -// "status": http.StatusInternalServerError, -// "message": fmt.Sprintf("Error getting favorite recipes. %s\n", err.Error()), -// }) -// return -// } -// -// // Get the engagement data, not sure what will happen when errors occur -// engagements, err := deps.EngagementService.GetUserEngagement(user.Id, 6) -// if err != nil { -// components.RenderErrorBanner(ctx, fmt.Sprintf("Error getting user engagements. %s\n", err.Error())) -// ctx.JSON(http.StatusInternalServerError, gin.H{ -// "status": http.StatusInternalServerError, -// "message": fmt.Sprintf("Error getting user engagements. %s\n", err.Error()), -// }) -// return -// } -// -// title := "Potion - Profile" -// page := pages.ProfilePage(*user, recipes, favorites, engagements) -// -// ctx.HTML(http.StatusOK, "", layouts.AppLayout(title, page)) -// } - -// DEPRECATED: As of September 4th, 2025. -// func ListPage(ctx *gin.Context) { -// title := "Potion - Shopping List" -// page := pages.ListPage() -// -// ctx.HTML(http.StatusOK, "", layouts.AppLayout(title, page)) -// } - -// DEPRECATED: As of September 4th, 2025. -// func RecipePage(ctx *gin.Context) { -// // Call recipe service to get via ID -// deps := ctx.MustGet("deps").(*domainServer.InjectedDependencies) -// id := ctx.Param("id") -// -// // Parse ID -// parsed, err := strconv.Atoi(id) -// if err != nil { -// components.RenderErrorBanner(ctx, fmt.Sprintf("ERROR: %s", err.Error())) -// ctx.JSON(400, err.Error()) -// return -// } -// -// // Get signed in user, if they exist -// var userId *int = nil -// var loggedIn = domainServer.IsLoggedIn(ctx) -// -// // Ensure user is logged in with a valid account -// if user := deps.UserService.GetAuthenicatedUser(ctx); user == nil { -// // Log (stale) user out -// domain.SetCookie(ctx, "jwt_token", "", -1) -// domain.SetCookie(ctx, "search-filters", "", -1) -// loggedIn = false -// } -// -// if loggedIn { -// storeId := ctx.MustGet("userId").(int) -// userId = &storeId -// } -// -// // Get recipe -// recipe, err := deps.RecipeService.GetRecipe(parsed, userId) -// if err != nil { -// components.RenderErrorBanner(ctx, fmt.Sprintf("ERROR: %s", err.Error())) -// ctx.JSON(400, err.Error()) -// return -// } -// -// // Get user (owner) -// user, err := deps.UserService.GetUser(recipe.UserId) -// if err != nil { -// components.RenderErrorBanner(ctx, fmt.Sprintf("ERROR: %s", err.Error())) -// ctx.JSON(400, err.Error()) -// return -// } -// -// title := "Potion - View Recipe" -// page := pages.RecipePage(*recipe, *user, loggedIn, deps.EnvironmentConfig.Domain) -// -// ctx.HTML(http.StatusOK, "", layouts.AppLayout(title, page)) -// } -// -// func SearchPage(ctx *gin.Context) { -// var page templ.Component -// // Get filters from cookies -// if bytes, err := ctx.Cookie("search-filters"); err != nil { -// fmt.Printf("ERROR: Failed to get search-filter cookie. %s\n", err.Error()) -// page = pages.SearchPage(nil, false) -// } else { -// var filters domainRecipe.SearchFilters -// if err := json.Unmarshal([]byte(bytes), &filters); err != nil { -// fmt.Printf("ERROR: Failed to unmarshal search-filter cookie. %s\n", err.Error()) -// page = pages.SearchPage(nil, false) -// } else { -// page = pages.SearchPage(&filters, true) -// } -// } -// -// title := "Potion - Recipe Search" -// -// ctx.HTML(http.StatusOK, "", layouts.AppLayout(title, page)) -// } - -// DEPRECATED: As of September 4th, 2025. -// func NotFoundPage(ctx *gin.Context) { -// title := "Potion - Not Found" -// page := pages.NotFoundPage() -// -// ctx.HTML(http.StatusOK, "", layouts.AppLayout(title, page)) -// } diff --git a/internal/app/handlers/recipe_handler.go b/internal/app/handlers/recipe_handler.go deleted file mode 100644 index 868de1b..0000000 --- a/internal/app/handlers/recipe_handler.go +++ /dev/null @@ -1,136 +0,0 @@ -package handlers - -// DEPRECATED: As of September 4th, 2025. -// const CREATE_ERROR_HTML = ` -//

-// Uh oh! Something went wrong when creating your recipe. Please try again. %s -//

-// ` - -// DEPRECATED: As of September 4th, 2025. -// func CreateRecipe(ctx *gin.Context) { -// deps := ctx.MustGet("deps").(*domain.InjectedDependencies) -// -// recipe, err := deps.RecipeService.CreateRecipe(ctx) -// if err != nil { -// components.RenderErrorBanner(ctx, err.Error()) -// ctx.String(http.StatusOK, CREATE_ERROR_HTML, err.Error()) -// return -// } -// -// // Send HTMX redirection -// url := fmt.Sprintf(domain.WEB_RECIPE, recipe.Id) -// ctx.Header("HX-Redirect", url) -// ctx.Status(http.StatusCreated) -// } - -// toBits converts an array of stringified numbers into a single summed value -// -// DEPRECATED: As of September 4th, 2025. -// func toBits(arr []string) (bits int) { -// for _, x := range arr { -// num, _ := strconv.Atoi(x) -// bits += num -// } -// return -// } - -// DEPRECATED: As of September 4th, 2025. -// func SearchRecipes(ctx *gin.Context) { -// deps := ctx.MustGet("deps").(*domain.InjectedDependencies) -// -// // create filters -// filters := domainRecipe.SearchFilters{ -// Search: ctx.PostForm("search"), // string, search query for titles -// MealType: toBits(ctx.PostFormArray("meal")), -// Time: toBits(ctx.PostFormArray("time")), -// Difficulty: toBits(ctx.PostFormArray("difficulty")), -// ServingSize: toBits(ctx.PostFormArray("serving")), -// } -// -// // Set the filters into the cookies, so they can be reloaded -// if bytes, err := json.Marshal(filters); err == nil { -// domain.SetCookie(ctx, "search-filters", string(bytes), time.Hour*24) -// // ctx.SetCookie( -// // "search-filters", -// // string(bytes), -// // int(time.Now().Add(24*time.Hour).Sub(time.Now()).Seconds()), -// // "/", -// // true, -// // ) -// } -// -// redirect := ctx.PostForm("redirect") -// if redirect == "true" { -// ctx.Header("HX-Redirect", domain.WEB_SEARCH) -// ctx.Status(http.StatusOK) -// return -// } -// -// // Get user if logged in, so we can get favorite status -// var userId *int = nil -// if domain.IsLoggedIn(ctx) { -// id := ctx.MustGet("userId").(int) -// userId = &id -// } -// -// // TODO: Not sure if we need to ensure the user is valid here -// -// // We don't care about favorite status, so use false -// recipes, err := deps.RecipeService.SearchRecipes(filters, userId, false) -// if err != nil { -// components.RenderErrorBanner(ctx, err.Error()) -// ctx.JSON(http.StatusOK, gin.H{"error": err.Error()}) -// } -// -// // Render content as the response -// ctx.Status(200) -// templates.ResultList(recipes).Render(ctx.Request.Context(), ctx.Writer) -// } - -// DEPRECATED: As of September 4th, 2025. -// func SearchRecipesFavorites(ctx *gin.Context) { -// deps := ctx.MustGet("deps").(*domain.InjectedDependencies) -// -// // create filters -// filters := domainRecipe.SearchFilters{ -// Search: ctx.PostForm("search"), // string, search query for titles -// MealType: toBits(ctx.PostFormArray("meal")), -// Time: toBits(ctx.PostFormArray("time")), -// Difficulty: toBits(ctx.PostFormArray("difficulty")), -// ServingSize: toBits(ctx.PostFormArray("serving")), -// } -// -// // Set the filters into the cookies, so they can be reloaded -// if bytes, err := json.Marshal(filters); err == nil { -// domain.SetCookie(ctx, "search-filters", string(bytes), time.Hour*24) -// // ctx.SetCookie( -// // "search-filters", -// // string(bytes), -// // int(time.Now().Add(24*time.Hour).Sub(time.Now()).Seconds()), -// // "/", -// // "", // TODO: Need an actual domain -// // false, // TODO: True in prod -// // true, -// // ) -// } -// -// // TODO: Error here if they're not logged in? -// // Get user data (they should be logged in) -// if !domain.IsLoggedIn(ctx) { -// components.RenderErrorBanner(ctx, "User is not logged in. User will be nil.") -// ctx.JSON(http.StatusOK, gin.H{"error": "User is not logged in. User will be nil."}) -// } -// -// userId := ctx.MustGet("userId").(int) -// -// recipes, err := deps.RecipeService.SearchRecipes(filters, &userId, true) -// if err != nil { -// components.RenderErrorBanner(ctx, err.Error()) -// ctx.JSON(http.StatusOK, gin.H{"error": err.Error()}) -// } -// -// // Render content as the response -// ctx.Status(200) -// templates.FavoriteList(recipes).Render(ctx.Request.Context(), ctx.Writer) -// } diff --git a/internal/app/handlers/state_handler.go b/internal/app/handlers/state_handler.go deleted file mode 100644 index d2c55b5..0000000 --- a/internal/app/handlers/state_handler.go +++ /dev/null @@ -1,71 +0,0 @@ -package handlers - -// DEPRECATED: As of September 4th, 2025. -// const TAG_HTML = ` -//
  • -// × %s -//
  • -// ` - -// DEPRECATED: As of September 4th, 2025. -// const TAG_LIST_HTML = ` -// -// ` - -// DEPRECATED: As of September 4th, 2025. -// func NewTag(ctx *gin.Context) { -// tag := strings.ToLower(ctx.PostForm("tag")) -// tags := strings.Split(ctx.PostForm("tags"), ",") -// -// tags = append([]string{tag}, tags...) -// -// var html string -// var cleaned_tags []string -// for _, tag := range tags { -// if tag != "" { -// html += fmt.Sprintf(TAG_HTML, domain.STATE_TAGS_DELETE, tag, tag) -// -// // Ensure that the list provided does not contain blank spaces. -// // This is another measure to ensure this state is bulletproof. -// cleaned_tags = append(cleaned_tags, tag) -// } -// } -// -// // Execute OOB swap for the tags -// html += fmt.Sprintf(TAG_LIST_HTML, strings.Join(cleaned_tags, ",")) -// -// ctx.String(http.StatusOK, html) -// } - -// DEPRECATED: As of September 4th, 2025. -// func DeleteTag(ctx *gin.Context) { -// tags := strings.Split(ctx.PostForm("tags"), ",") -// target := ctx.PostForm("target") -// -// var html string -// var new_tags []string -// for _, tag := range tags { -// if tag != target && tag != "" { -// html += fmt.Sprintf(TAG_HTML, domain.STATE_TAGS_DELETE ,tag, tag) -// new_tags = append(new_tags, tag) -// } -// } -// -// // Execute OOB swap for the tags -// html += fmt.Sprintf(TAG_LIST_HTML, strings.Join(new_tags, ",")) -// -// ctx.String(http.StatusOK, html) -// } diff --git a/internal/app/handlers/user_handler.go b/internal/app/handlers/user_handler.go deleted file mode 100644 index d63dd61..0000000 --- a/internal/app/handlers/user_handler.go +++ /dev/null @@ -1,83 +0,0 @@ -package handlers - -// DEPRECATED: As of September 4th, 2025. -// func GetUserRecipes(ctx *gin.Context) { -// deps := ctx.MustGet("deps").(*domain.InjectedDependencies) -// -// // Ensure user is logged in with a valid account -// user := deps.UserService.GetAuthenicatedUser(ctx) -// if user == nil { -// // Log (stale) user out -// domain.SetCookie(ctx, "jwt_token", "", -1) -// domain.SetCookie(ctx, "search-filters", "", -1) -// } -// -// // Ensure logged in -// if !domain.IsLoggedIn(ctx) || user == nil { -// components.RenderErrorBanner(ctx, "User is not authorized to access this endpoint. Please login to continue.") -// ctx.JSON(http.StatusUnauthorized, gin.H{ -// "status": http.StatusUnauthorized, -// "message": "User is not authorized to access this endpoint. Please login to continue.", -// "recipes": nil, -// }) -// return -// } -// -// recipes, err := deps.RecipeService.GetUserRecipes(user.Id) -// if err != nil { -// components.RenderErrorBanner(ctx, fmt.Sprintf("Could not get user recipes. %s", err.Error())) -// ctx.JSON(http.StatusBadRequest, gin.H{ -// "status": http.StatusBadRequest, -// "message": fmt.Sprintf("Could not get user recipes. %s", err.Error()), -// "recipes": nil, -// }) -// return -// } -// -// ctx.JSON(http.StatusOK, gin.H{ -// "status": http.StatusOK, -// "message": "User recipes successfully retrieved.", -// "recipes": recipes, -// }) -// } - -// DEPRECATED: As of September 4th, 2025. -// func GetUserFavoriteRecipes(ctx *gin.Context) { -// deps := ctx.MustGet("deps").(*domain.InjectedDependencies) -// -// // Ensure user is logged in with a valid account -// user := deps.UserService.GetAuthenicatedUser(ctx) -// if user == nil { -// // Log (stale) user out -// domain.SetCookie(ctx, "jwt_token", "", -1) -// domain.SetCookie(ctx, "search-filters", "", -1) -// } -// -// // Ensure logged in -// if !domain.IsLoggedIn(ctx) || user == nil { -// components.RenderErrorBanner(ctx, "User is not authorized to access this endpoint. Please login to continue.") -// ctx.JSON(http.StatusUnauthorized, gin.H{ -// "status": http.StatusUnauthorized, -// "message": "User is not authorized to access this endpoint. Please login to continue.", -// "recipes": nil, -// }) -// return -// } -// -// recipes, err := deps.RecipeService.GetUserFavoriteRecipes(user.Id) -// if err != nil { -// components.RenderErrorBanner(ctx, fmt.Sprintf("Could not get favorite recipes. %s", err.Error())) -// ctx.JSON(http.StatusBadRequest, gin.H{ -// "status": http.StatusBadRequest, -// "message": fmt.Sprintf("Could not get favorite recipes. %s", err.Error()), -// "recipes": nil, -// }) -// return -// } -// -// ctx.JSON(http.StatusOK, gin.H{ -// "status": http.StatusOK, -// "message": "User recipes successfully retrieved.", -// "recipes": recipes, -// }) -// } diff --git a/internal/app/server/auth_handler_v2.go b/internal/app/server/auth_handler_v2.go new file mode 100644 index 0000000..5ab70f3 --- /dev/null +++ b/internal/app/server/auth_handler_v2.go @@ -0,0 +1,48 @@ +package server + +import ( + "fmt" + "net/http" + "net/url" + "time" + + "github.com/gin-gonic/gin" +) + +// GetGoogleAuthUrlHandlerV2 fetches a Google authentication URl and returns it. +// This function is atomic and cannot fail. +func (s *Server) GetGoogleAuthUrlHandlerV2(ctx *gin.Context) { + url := s.deps.AuthService.GetGoogleAuthUrl() + ctx.JSON(http.StatusOK, gin.H{ + "status": http.StatusOK, + "message": "[OK] Successfully retrieved Google auth URL.", + "url": url, + }) +} + +// GoogleCallbackHandlerV2 reads the data from the Google redirection and uses it +// to generate a JWT which is sent back to the UI via a URL query parameter. If an +// error occurs the user will be directed to the login page with an error query param. +func (s *Server) GoogleCallbackHandlerV2(ctx *gin.Context) { + var ( + state string = ctx.Query("state") + code string = ctx.Query("code") + ) + + domain := s.deps.EnvironmentConfig.FrontendDomain + + if jwt, err := s.deps.AuthService.GoogleAuthSuccess(state, code); err != nil { + url := fmt.Sprintf("%s/v2/web/login?error=%s", domain, url.QueryEscape(err.Error())) + ctx.Redirect(http.StatusSeeOther, url) + } else { + url := fmt.Sprintf("%s/v2/web/home", domain) + s.SetCookie(ctx, "jwt_token", jwt, time.Hour*24*7) + ctx.Redirect(http.StatusSeeOther, url) + } +} + +func (s *Server) LogoutHandlerV2(ctx *gin.Context) { + s.SetCookie(ctx, "jwt_token", "", -1) + // s.SetCookie(ctx, "search-filters", "", -1) // TODO: This was copied, might function differently now + ctx.Status(http.StatusNoContent) +} diff --git a/internal/app/server/authentication.go b/internal/app/server/authentication.go new file mode 100644 index 0000000..81d2283 --- /dev/null +++ b/internal/app/server/authentication.go @@ -0,0 +1,49 @@ +package server + +import ( + "net/http" + + "github.com/gin-gonic/gin" + domain "github.com/haydenhargreaves/Potion/internal/domain/user" +) + +// AuthenticatedFunc is a function that handles authenticated requests +type AuthenticatedFunc func(ctx *gin.Context, user *domain.User) + +// withAuthenticatedUser is a helper to run a handler only if user is authenticated. Otherwise +// the function will return an error with a 401 status. +// +// BUG: This is probably not very effecient, since we hit the DB on every single protected request. +// +// If this ends up being a bottle neck we could simply hit the context for the userId, since +// that is usually all we need...Or maybe have two methods, for those that need the whole user +// and those that just need the ID. +func (s *Server) withAuthenticatedUser(ctx *gin.Context, handler AuthenticatedFunc) { + user := s.deps.UserService.GetAuthenicatedUser(ctx) + if user == nil { + // User is stale, ensure they are logged out so they can be prompted to log back in + s.SetCookie(ctx, "jwt_token", "", -1) + // s.SetCookie(ctx, "search-filters", "", -1) // TODO: Might need this again + + ctx.JSON(http.StatusUnauthorized, gin.H{ + "status": http.StatusUnauthorized, + "message": "[UNAUTHORIZED] Could not fetch authenticated user.", + }) + return + } + handler(ctx, user) +} + +// getUserId retrieves the userId from the context and returns a pointer to it. A nil +// pointer can be returned and will if the userId does not exist. +func getUserId(ctx *gin.Context) *int { + userIdAny, exists := ctx.Get("userId") + if !exists { + return nil + } + userIdInt, ok := userIdAny.(int) + if !ok { + return nil + } + return &userIdInt +} diff --git a/internal/app/server/cookies.go b/internal/app/server/cookies.go index 8cb46b3..78ac8f6 100644 --- a/internal/app/server/cookies.go +++ b/internal/app/server/cookies.go @@ -1,6 +1,7 @@ package server import ( + "net/http" "time" "github.com/gin-gonic/gin" @@ -18,10 +19,10 @@ import ( func (s *Server) SetCookie(ctx *gin.Context, name, value string, duration time.Duration) { var ( path string = "/" - httpOnly bool = true + httpOnly bool = false // NOTE: Should use false so React can see it! maxAge int - secure bool - domain string + secure bool = true + domain string = "" ) if duration < 0 { @@ -32,22 +33,22 @@ func (s *Server) SetCookie(ctx *gin.Context, name, value string, duration time.D maxAge = 0 } else { // Normal calculation - maxAge = int(time.Now().Add(duration).Sub(time.Now()).Seconds()) + maxAge = int(time.Until(time.Now().Add(duration)).Seconds()) } + // TODO: This whole system is stupid now if s.deps.EnvironmentConfig.Environment == "prod" { secure = true - domain = s.deps.EnvironmentConfig.Domain + // domain = "potion.gophernest" + // domain = s.deps.EnvironmentConfig.Domain + domain = ".gophernest.net" } else if s.deps.EnvironmentConfig.Environment == "dev" { secure = false - domain = s.deps.EnvironmentConfig.Domain - - } else { - // Defaults - secure = false - domain = "" + // domain = s.deps.EnvironmentConfig.Domain + domain = "localhost" } + ctx.SetSameSite(http.SameSiteNoneMode) ctx.SetCookie(name, value, maxAge, path, domain, secure, httpOnly) } diff --git a/internal/app/server/engagement_handler_v2.go b/internal/app/server/engagement_handler_v2.go new file mode 100644 index 0000000..2fcd806 --- /dev/null +++ b/internal/app/server/engagement_handler_v2.go @@ -0,0 +1,143 @@ +package server + +import ( + "fmt" + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + domain "github.com/haydenhargreaves/Potion/internal/domain/user" +) + +func (s *Server) EngagementViewRecipeHandlerV2(ctx *gin.Context) { + recipeId, err := strconv.Atoi(ctx.Param("id")) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{ + "status": http.StatusBadRequest, + "message": fmt.Sprintf("[ERROR] Failed to parse ID parameter. %s", err.Error()), + }) + return + } + + userId := getUserId(ctx) + if userId == nil { + if engagement, err := s.deps.EngagementService.ViewRecipe(recipeId); err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{ + "status": http.StatusBadRequest, + "message": fmt.Sprintf("[ERROR] Failed to create engagement. %s", err.Error()), + }) + } else { + ctx.JSON(http.StatusOK, gin.H{ + "status": http.StatusOK, + "message": "[OK] Successfully created engagement.", + "engagement": engagement, + }) + } + return + } + if engagement, err := s.deps.EngagementService.UserViewRecipe(*userId, recipeId); err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{ + "status": http.StatusBadRequest, + "message": fmt.Sprintf("[ERROR] Failed to create engagement. %s", err.Error()), + }) + } else { + ctx.JSON(http.StatusOK, gin.H{ + "status": http.StatusOK, + "message": "[OK] Successfully created engagement.", + "engagement": engagement, + }) + } +} + +func (s *Server) EngagementShareRecipeHandlerV2(ctx *gin.Context) { + recipeId, err := strconv.Atoi(ctx.Param("id")) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{ + "status": http.StatusBadRequest, + "message": fmt.Sprintf("[ERROR] Failed to parse ID parameter. %s", err.Error()), + }) + return + } + + userId := getUserId(ctx) + if userId == nil { + if engagement, err := s.deps.EngagementService.ShareRecipe(recipeId); err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{ + "status": http.StatusBadRequest, + "message": fmt.Sprintf("[ERROR] Failed to create engagement. %s", err.Error()), + }) + } else { + ctx.JSON(http.StatusOK, gin.H{ + "status": http.StatusOK, + "message": "[OK] Successfully created engagement.", + "engagement": engagement, + }) + } + return + } + + if engagement, err := s.deps.EngagementService.UserShareRecipe(*userId, recipeId); err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{ + "status": http.StatusBadRequest, + "message": fmt.Sprintf("[ERROR] Failed to create engagement. %s", err.Error()), + }) + } else { + ctx.JSON(http.StatusOK, gin.H{ + "status": http.StatusOK, + "message": "[OK] Successfully created engagement.", + "engagement": engagement, + }) + } +} + +func (s *Server) EngagementFavoriteRecipeHandlerV2(ctx *gin.Context) { + s.withAuthenticatedUser(ctx, func(ctx *gin.Context, user *domain.User) { + recipeId, err := strconv.Atoi(ctx.Param("id")) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{ + "status": http.StatusBadRequest, + "message": fmt.Sprintf("[ERROR] Failed to parse ID parameter. %s", err.Error()), + }) + return + } + + if engagement, err := s.deps.EngagementService.UserFavoriteRecipe(user.Id, recipeId); err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{ + "status": http.StatusBadRequest, + "message": fmt.Sprintf("[ERROR] Failed to create engagement. %s", err.Error()), + }) + } else { + ctx.JSON(http.StatusOK, gin.H{ + "status": http.StatusOK, + "message": "[OK] Successfully created engagement.", + "engagement": engagement, + }) + } + }) +} + +func (s *Server) EngagementMakeRecipeHandlerV2(ctx *gin.Context) { + s.withAuthenticatedUser(ctx, func(ctx *gin.Context, user *domain.User) { + recipeId, err := strconv.Atoi(ctx.Param("id")) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{ + "status": http.StatusBadRequest, + "message": fmt.Sprintf("[ERROR] Failed to parse ID parameter. %s", err.Error()), + }) + return + } + + if engagement, err := s.deps.EngagementService.UserMakeRecipe(user.Id, recipeId); err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{ + "status": http.StatusBadRequest, + "message": fmt.Sprintf("[ERROR] Failed to create engagement. %s", err.Error()), + }) + } else { + ctx.JSON(http.StatusOK, gin.H{ + "status": http.StatusOK, + "message": "[OK] Successfully created engagement.", + "engagement": engagement, + }) + } + }) +} diff --git a/internal/app/server/middleware_v2.go b/internal/app/server/middleware_v2.go new file mode 100644 index 0000000..fbdaa5f --- /dev/null +++ b/internal/app/server/middleware_v2.go @@ -0,0 +1,98 @@ +package server + +import ( + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/golang-jwt/jwt/v5" + domain "github.com/haydenhargreaves/Potion/internal/domain/server" +) + +// JwtAuthMiddlewareV2 is responsible for protecting routes. Anything that may go wrong +// will be returned via JSON with a 'message' field and a 401 error code. When this +// middleware is successful, it will set the 'userId' and 'userEmail' fields and pass +// to the next function in the chain. +// +// Functions that are called after this can assume that those values defined are always +// set. +func JwtAuthMiddlewareV2(jwtSecretKey []byte) gin.HandlerFunc { + return func(ctx *gin.Context) { + tokenString, err := ctx.Cookie("jwt_token") + if err != nil { + ctx.JSON(http.StatusUnauthorized, gin.H{ + "status": http.StatusUnauthorized, + "message": fmt.Sprintf("[UNAUTHORIZED] Failed to get token from cookie. %s", err.Error()), + }) + ctx.Abort() + return + } + + claims := &domain.JwtClaims{} + + token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + return jwtSecretKey, nil + }) + + // Error occurred when parsing + if err != nil { + ctx.JSON(http.StatusUnauthorized, gin.H{ + "status": http.StatusUnauthorized, + "message": fmt.Sprintf("[UNAUTHORIZED] Error parsing cooking. %s", err.Error()), + }) + ctx.Abort() + return + } + + // Token is invalid + if !token.Valid { + ctx.JSON(http.StatusUnauthorized, gin.H{ + "status": http.StatusUnauthorized, + "message": "[UNAUTHORIZED] Token is invalid.", + }) + ctx.Abort() + return + } + + // Found: Set the values + ctx.Set("userId", claims.UserId) + ctx.Set("userEmail", claims.Email) + ctx.Next() + } +} + +// JwtOptionalAuthMiddlewareV2 is responsible for collecting user data for routes where +// authentication is optional. Meaning: if the use is not logged in, this function does +// not fail or return, it simply does nothing. But if the user is logged in, then the +// 'userId' and 'userEmail' context values are set. +// +// e.g., `userIdAny, exists := ctx.Get("userId")` +func JwtOptionalAuthMiddlewareV2(jwtSecretKey []byte) gin.HandlerFunc { + return func(ctx *gin.Context) { + tokenString, err := ctx.Cookie("jwt_token") + if err != nil || tokenString == "" { + // No cookie found: not authenticated, but allow access + ctx.Next() + return + } + + claims := &domain.JwtClaims{} + token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + return jwtSecretKey, nil + }) + + if err == nil && token.Valid { + // Set user info in context if token is valid + ctx.Set("userId", claims.UserId) + ctx.Set("userEmail", claims.Email) + } + // Otherwise, just continue (user is unauthenticated) + ctx.Next() + } +} diff --git a/internal/app/server/recipe_handler_v2.go b/internal/app/server/recipe_handler_v2.go new file mode 100644 index 0000000..7c1a9fb --- /dev/null +++ b/internal/app/server/recipe_handler_v2.go @@ -0,0 +1,113 @@ +package server + +import ( + "fmt" + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + domain "github.com/haydenhargreaves/Potion/internal/domain/recipe" +) + +// GetRecipeOfTheWeekHandler fetchs the current recipe of the week and returns it. +// If an error occurs, it will be returned and a recipe will not be returned. +// +// BUG: Until auth is reimplemented, there is no way to determine what user is making the +// call. +// NOTE: I believe this issue has been resolved +func (s *Server) GetRecipeOfTheWeekHandlerV2(ctx *gin.Context) { + userId := getUserId(ctx) + recipe, err := s.deps.RecipeService.GetRecipeOfTheWeek(userId) + + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{ + "status": http.StatusBadRequest, + "message": fmt.Sprintf("[ERROR] Failed to get recipe of the week. %s", err.Error()), + }) + return + } + + ctx.JSON(http.StatusOK, gin.H{ + "status": http.StatusOK, + "message": "[OK] Successfully retrieved recipe of the week.", + "recipe": recipe, + }) +} + +func (s *Server) GetRecipeHandlerV2(ctx *gin.Context) { + id := ctx.Param("id") + parsedId, err := strconv.Atoi(id) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{ + "status": http.StatusBadRequest, + "message": fmt.Sprintf("[ERROR] Failed to parse ID parameter. %s", err.Error()), + }) + return + } + + userId := getUserId(ctx) + recipe, err := s.deps.RecipeService.GetRecipe(parsedId, userId) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{ + "status": http.StatusBadRequest, + "message": fmt.Sprintf("[ERROR] Failed to get recipe. %s", err.Error()), + }) + return + } + + ctx.JSON(http.StatusOK, gin.H{ + "status": http.StatusOK, + "message": "[OK] Successfully retrieved recipe.", + "recipe": recipe, + }) +} + +func (s *Server) SearchRecipeHandlerV2(ctx *gin.Context) { + var filters domain.SearchFilters + + // Parse filters + if err := ctx.ShouldBindJSON(&filters); err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{ + "status": http.StatusBadRequest, + "message": fmt.Sprintf("[ERROR] Failed to parse filters. %s", err.Error()), + }) + return + } + + // This is optional, so we can do this + userId := getUserId(ctx) + + // Did I really have two APIs...? + // TODO: Fix service at some point, no need to accept the favorites (bool) param + recipes, err := s.deps.RecipeService.SearchRecipes(filters, userId, filters.Favorites) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{ + "status": http.StatusBadRequest, + "message": fmt.Sprintf("[ERROR] Failed to get searched recipes. %s", err.Error()), + }) + return + } + + ctx.JSON(http.StatusOK, gin.H{ + "status": http.StatusOK, + "message": "[OK] Successfully retrieved recipes based on provided filters.", + "recipes": recipes, + }) +} + +func (s *Server) CreateRecipeHandlerV2(ctx *gin.Context) { + recipe, err := s.deps.RecipeService.CreateRecipe(ctx) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{ + "status": http.StatusBadRequest, + "message": fmt.Sprintf("[ERROR] Failed to create recipe. %s", err.Error()), + }) + return + } + + ctx.JSON(http.StatusOK, gin.H{ + "status": http.StatusOK, + "message": "[OK] Successfully created new recipe.", + "recipe": recipe, + }) +} diff --git a/internal/app/server/server.go b/internal/app/server/server.go index 3e8f85f..a2a7b52 100644 --- a/internal/app/server/server.go +++ b/internal/app/server/server.go @@ -42,7 +42,11 @@ func Init(port int) *Server { server.Router.SetTrustedProxies(nil) // Setup the CORS settings and active them - server.config.AllowAllOrigins = true + // server.config.AllowAllOrigins = true + server.config.AllowOrigins = []string{"http://localhost:5173", "https://potion.gophernest.net"} + server.config.AllowMethods = []string{"GET", "POST", "PUT", "DELETE"} + server.config.AllowHeaders = []string{"Origin", "Content-Type", "Authorization"} + server.config.AllowCredentials = true server.Router.Use(cors.New(server.config)) return server @@ -74,7 +78,8 @@ func (s *Server) Setup() *Server { // SETUP GOOGLE AUTH var ( - redirectUrl string = fmt.Sprintf("%s%s", cfg.Domain, domain.API_AUTH_CALLBACK) + // NOTE: USING V2 NOW + redirectUrl string = fmt.Sprintf("%s%s", cfg.Domain, domain.API_AUTH_CALLBACK_V2) clientId string = cfg.GoogleClientId clientSecret string = cfg.GoogleClientSecret scope []string = []string{ @@ -120,13 +125,15 @@ func (s *Server) Setup() *Server { // Apply middleware s.Router.Use(RecoveryMiddleware()) - s.Router.Use(JwtAuthMiddleWare(jwtSecret)) + // NOTE: No longer running on every connection! + // s.Router.Use(JwtAuthMiddleWare(jwtSecret)) // Redirect index to home page: Update this as needed s.Router.GET("/", func(ctx *gin.Context) { ctx.Redirect(http.StatusSeeOther, domain.WEB_HOME) }) // Wrap all routes with a version - router_v1 := s.Router.Group(domain.VERSION) + router_v1 := s.Router.Group(domain.VERSION_1) + router_v2 := s.Router.Group(domain.VERSION_2) // Domain specific routers router_web := router_v1.Group(domain.WEB) @@ -179,7 +186,7 @@ func (s *Server) Setup() *Server { path := ctx.Request.URL.Path // TODO: Use constants for errors? - if strings.HasPrefix(path, domain.VERSION+domain.API) { + if strings.HasPrefix(path, domain.VERSION_1+domain.API) { ctx.JSON(http.StatusNotFound, gin.H{ "status": http.StatusNotFound, "error": "API_NOT_FOUND", @@ -192,5 +199,34 @@ func (s *Server) Setup() *Server { ctx.Redirect(http.StatusSeeOther, domain.WEB_NOT_FOUND) }) + // ---- VERSION 2 ROUTES ---- // + router_api_v2 := router_v2.Group(domain.API) + router_api_v2.GET("/recipe/:id", JwtOptionalAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.GetRecipeHandlerV2) + router_api_v2.GET("/recipe/of-the-week", JwtOptionalAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.GetRecipeOfTheWeekHandlerV2) + router_api_v2.POST("/recipe/search", JwtOptionalAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.SearchRecipeHandlerV2) + router_api_v2.POST("/recipe", JwtAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.CreateRecipeHandlerV2) + + router_api_v2.GET("/auth/login", s.GetGoogleAuthUrlHandlerV2) + router_api_v2.GET("/auth/callback", s.GoogleCallbackHandlerV2) + router_api_v2.GET("/auth/logout", s.LogoutHandlerV2) + + router_api_v2.GET("/user", JwtAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.GetAuthenticatedUserHandlerV2) + router_api_v2.GET("/user/:id", s.GetUserV2) + router_api_v2.GET("/user/recipes", JwtAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.GetAuthenicatedUserRecipesV2) + router_api_v2.GET("/user/favorites", JwtAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.GetAuthenicatedUserFavoritesV2) + router_api_v2.GET("/user/engagement", JwtAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.GetAuthenicatedUserEngagementV2) + + router_api_v2.GET("/user/recipes/made", JwtAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.GetAuthenicatedUserMadeRecipesV2) + router_api_v2.GET("/user/recipes/viewed", JwtAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.GetAuthenicatedUserViewedRecipesV2) + + router_api_v2.GET("/protected", JwtAuthMiddlewareV2([]byte(cfg.JwtSecret)), func(ctx *gin.Context) { + ctx.JSON(http.StatusOK, gin.H{"msg": "YAY"}) + }) + + router_api_v2.POST("/engagement/view/:id", JwtOptionalAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.EngagementViewRecipeHandlerV2) + router_api_v2.POST("/engagement/share/:id", JwtOptionalAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.EngagementShareRecipeHandlerV2) + router_api_v2.POST("/engagement/favorite/:id", JwtAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.EngagementFavoriteRecipeHandlerV2) + router_api_v2.POST("/engagement/make/:id", JwtAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.EngagementMakeRecipeHandlerV2) + return s } diff --git a/internal/app/server/user_handler_v2.go b/internal/app/server/user_handler_v2.go new file mode 100644 index 0000000..120e04a --- /dev/null +++ b/internal/app/server/user_handler_v2.go @@ -0,0 +1,142 @@ +package server + +import ( + "fmt" + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + domain "github.com/haydenhargreaves/Potion/internal/domain/user" +) + +func (s *Server) GetUserV2(ctx *gin.Context) { + id := ctx.Param("id") + parsedId, err := strconv.Atoi(id) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{ + "status": http.StatusBadRequest, + "message": fmt.Sprintf("[ERROR] Failed to parse ID parameter. %s", err.Error()), + }) + return + } + + user, err := s.deps.UserService.GetUser(parsedId) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{ + "status": http.StatusBadRequest, + "message": fmt.Sprintf("[ERROR] Failed to get the target user. %s", err.Error()), + }) + return + } + + ctx.JSON(http.StatusOK, gin.H{ + "status": http.StatusOK, + "message": "[OK] Successfully retrieved target user.", + "user": user, + }) +} + +func (s *Server) GetAuthenticatedUserHandlerV2(ctx *gin.Context) { + s.withAuthenticatedUser(ctx, func(ctx *gin.Context, user *domain.User) { + ctx.JSON(http.StatusOK, gin.H{ + "status": http.StatusOK, + "message": "[OK] Successfully retrieved authenticated user.", + "user": user, + }) + }) +} + +func (s *Server) GetAuthenicatedUserRecipesV2(ctx *gin.Context) { + s.withAuthenticatedUser(ctx, func(ctx *gin.Context, user *domain.User) { + recipes, err := s.deps.RecipeService.GetUserRecipes(user.Id) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{ + "status": http.StatusBadRequest, + "message": fmt.Sprintf("[ERROR] Could not fetch authenticated users's recipes. %s", err.Error()), + }) + return + } + + ctx.JSON(http.StatusOK, gin.H{ + "status": http.StatusOK, + "message": "[OK] Successfully retrieved authenticated user's recipes.", + "recipes": recipes, + }) + }) +} + +func (s *Server) GetAuthenicatedUserFavoritesV2(ctx *gin.Context) { + s.withAuthenticatedUser(ctx, func(ctx *gin.Context, user *domain.User) { + favorites, err := s.deps.RecipeService.GetUserFavoriteRecipes(user.Id) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{ + "status": http.StatusBadRequest, + "message": fmt.Sprintf("[ERROR] Could not fetch authenticated users's favorites. %s", err.Error()), + }) + return + } + + ctx.JSON(http.StatusOK, gin.H{ + "status": http.StatusOK, + "message": "[OK] Successfully retrieved authenticated user's favorites.", + "favorites": favorites, + }) + }) +} + +func (s *Server) GetAuthenicatedUserEngagementV2(ctx *gin.Context) { + s.withAuthenticatedUser(ctx, func(ctx *gin.Context, user *domain.User) { + engagement, err := s.deps.EngagementService.GetUserEngagement(user.Id, 6) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{ + "status": http.StatusBadRequest, + "message": fmt.Sprintf("[ERROR] Failed to get authenticated user engagement. %s", err.Error()), + }) + return + } + + ctx.JSON(http.StatusOK, gin.H{ + "status": http.StatusOK, + "message": "[OK] Successfully retrieved authenticated user engagement.", + "engagement": engagement, + }) + }) +} + +func (s *Server) GetAuthenicatedUserMadeRecipesV2(ctx *gin.Context) { + s.withAuthenticatedUser(ctx, func(ctx *gin.Context, user *domain.User) { + recipes, err := s.deps.RecipeService.GetUserMadeRecipes(user.Id, 6) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{ + "status": http.StatusBadRequest, + "message": fmt.Sprintf("[ERROR] Failed to get authenticated user's made recipes. %s", err.Error()), + }) + return + } + + ctx.JSON(http.StatusOK, gin.H{ + "status": http.StatusOK, + "message": "[OK] Successfully retrieved authenticated user's made recipes.", + "recipes": recipes, + }) + }) +} + +func (s *Server) GetAuthenicatedUserViewedRecipesV2(ctx *gin.Context) { + s.withAuthenticatedUser(ctx, func(ctx *gin.Context, user *domain.User) { + recipes, err := s.deps.RecipeService.GetUserViewedRecipes(user.Id, 6) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{ + "status": http.StatusBadRequest, + "message": fmt.Sprintf("[ERROR] Failed to get authenticated user's viewed recipes. %s", err.Error()), + }) + return + } + + ctx.JSON(http.StatusOK, gin.H{ + "status": http.StatusOK, + "message": "[OK] Successfully retrieved authenticated user's viewed recipes.", + "recipes": recipes, + }) + }) +} diff --git a/internal/app/service/recipe_service.go b/internal/app/service/recipe_service.go index fc89a87..acb5baa 100644 --- a/internal/app/service/recipe_service.go +++ b/internal/app/service/recipe_service.go @@ -1,11 +1,7 @@ package service import ( - "errors" "fmt" - "net/http" - "strconv" - "strings" "time" "github.com/gin-gonic/gin" @@ -45,66 +41,25 @@ func (s *RecipeService) CreateRecipe(ctx *gin.Context) (*domain.Recipe, error) { return nil, fmt.Errorf("User is not logged in.") } - title := ctx.PostForm("title") - description := ctx.PostForm("description") - preparation := ctx.PostForm("preparation-time") - cook := ctx.PostForm("cook-time") - serving := ctx.PostForm("serving-size") - category := ctx.PostForm("category") - difficulty := ctx.PostForm("difficulty") - ingredients := ctx.PostFormArray("ingredients") - quantity := ctx.PostFormArray("quantity") - instructions := ctx.PostFormArray("instructions") - tags := strings.Split(ctx.PostForm("tags"), ",") userId := ctx.MustGet("userId").(int) + var req domain.CreateRecipeRequest - // Have to get the image differently - image, err := ctx.FormFile("image") - if err != nil && !errors.Is(err, http.ErrMissingFile) { - // Error getting image + if err := ctx.ShouldBindJSON(&req); err != nil { + return nil, err } - // Convert to proper values - servingInt, _ := strconv.Atoi(serving) - difficultyInt, _ := strconv.Atoi(difficulty) - prepInt, _ := strconv.Atoi(preparation) - cookInt, _ := strconv.Atoi(cook) - - var ingredientSlice []domain.RecipeIngredient - for i := range len(ingredients) { - if strings.TrimSpace(ingredients[i]) != "" { - ins := domain.RecipeIngredient{ - Name: ingredients[i], - Quantity: quantity[i], - } - - ingredientSlice = append(ingredientSlice, ins) - } - } - - var instructionSlice []string - for _, ins := range instructions { - if ins != "" { - instructionSlice = append(instructionSlice, ins) - } - } - - // Create the recipe recipe := domain.Recipe{ - Title: title, - Description: description, - Instructions: instructionSlice, - Serves: servingInt, - Difficulty: difficultyInt, - Duration: domain.RecipeDuration{ - Total: prepInt + cookInt, - Prep: prepInt, - Cook: cookInt, - }, - Category: domain.RecipeMeal(category), - Ingredients: ingredientSlice, - UserId: userId, - Created: time.Now(), + Title: req.Title, + Description: req.Description, + Instructions: req.Instructions, + Serves: req.Serves, + Difficulty: req.Difficulty, + Duration: req.Duration, + Category: req.Category, + Ingredients: req.Ingredients, + Sections: req.Sections, + UserId: userId, + Created: time.Now(), } if err := s.recipeRepository.CreateRecipe(&recipe); err != nil { @@ -112,17 +67,96 @@ func (s *RecipeService) CreateRecipe(ctx *gin.Context) (*domain.Recipe, error) { } // TODO: Upload the image - if image != nil { - } + // if req.image != nil { + // } // Create the tags - if len(tags) > 0 { - if err := s.recipeRepository.CreateRecipeTags(recipe, tags); err != nil { + if len(req.Tags) > 0 { + if err := s.recipeRepository.CreateRecipeTags(recipe, req.Tags); err != nil { return &recipe, fmt.Errorf("Failed to attach/create tags. %s\n", err.Error()) } } return &recipe, nil + + // title := ctx.PostForm("title") + // description := ctx.PostForm("description") + // preparation := ctx.PostForm("preparation-time") + // cook := ctx.PostForm("cook-time") + // serving := ctx.PostForm("serving-size") + // category := ctx.PostForm("category") + // difficulty := ctx.PostForm("difficulty") + // ingredients := ctx.PostFormArray("ingredients") + // quantity := ctx.PostFormArray("quantity") + // instructions := ctx.PostFormArray("instructions") + // tags := strings.Split(ctx.PostForm("tags"), ",") + // userId := ctx.MustGet("userId").(int) + // + // // Have to get the image differently + // image, err := ctx.FormFile("image") + // if err != nil && !errors.Is(err, http.ErrMissingFile) { + // // Error getting image + // } + // + // // Convert to proper values + // servingInt, _ := strconv.Atoi(serving) + // difficultyInt, _ := strconv.Atoi(difficulty) + // prepInt, _ := strconv.Atoi(preparation) + // cookInt, _ := strconv.Atoi(cook) + // + // var ingredientSlice []domain.RecipeIngredient + // for i := range len(ingredients) { + // if strings.TrimSpace(ingredients[i]) != "" { + // ins := domain.RecipeIngredient{ + // Name: ingredients[i], + // Quantity: quantity[i], + // } + // + // ingredientSlice = append(ingredientSlice, ins) + // } + // } + // + // var instructionSlice []string + // for _, ins := range instructions { + // if ins != "" { + // instructionSlice = append(instructionSlice, ins) + // } + // } + // + // // Create the recipe + // recipe := domain.Recipe{ + // Title: title, + // Description: description, + // Instructions: instructionSlice, + // Serves: servingInt, + // Difficulty: difficultyInt, + // Duration: domain.RecipeDuration{ + // Total: prepInt + cookInt, + // Prep: prepInt, + // Cook: cookInt, + // }, + // Category: domain.RecipeMeal(category), + // Ingredients: ingredientSlice, + // UserId: userId, + // Created: time.Now(), + // } + // + // if err := s.recipeRepository.CreateRecipe(&recipe); err != nil { + // return &recipe, err + // } + // + // // TODO: Upload the image + // if image != nil { + // } + // + // // Create the tags + // if len(tags) > 0 { + // if err := s.recipeRepository.CreateRecipeTags(recipe, tags); err != nil { + // return &recipe, fmt.Errorf("Failed to attach/create tags. %s\n", err.Error()) + // } + // } + // + // return &recipe, nil } // GetRecipe will get a recipe via its ID. Any errors will be bubbled to the caller. Furthermore, @@ -135,7 +169,7 @@ func (s *RecipeService) CreateRecipe(ctx *gin.Context) (*domain.Recipe, error) { func (s *RecipeService) GetRecipe(id int, userId *int) (*domain.Recipe, error) { recipe, err := s.recipeRepository.GetRecipe(id, userId) - if recipe == nil { + if recipe == nil && err == nil { return nil, fmt.Errorf("Recipe does not exist or has been relocated. Please try again.") } @@ -159,19 +193,34 @@ func (s *RecipeService) GetRecipe(id int, userId *int) (*domain.Recipe, error) { // // The favorites parameter is used to only return filters favorited by the userId provided. func (s *RecipeService) SearchRecipes(filters domain.SearchFilters, userId *int, favorites bool) ([]domain.Recipe, error) { - return s.recipeRepository.SearchRecipes(filters, userId, favorites) + ids, err := s.recipeRepository.SearchRecipes(filters, userId, favorites) + if err != nil { + return []domain.Recipe{}, err + } + + return s.recipeRepository.GetRecipes(ids, userId) } // GetUserRecipes returns a list of the recipes that the user has created. The user's // ID should be provided. Any errors will be bubbled to the caller. -func (s *RecipeService) GetUserRecipes(id int) ([]domain.Recipe, error) { - return s.recipeRepository.GetUserRecipes(id) +func (s *RecipeService) GetUserRecipes(userId int) ([]domain.Recipe, error) { + ids, err := s.recipeRepository.GetUserRecipesIds(userId) + if err != nil { + return []domain.Recipe{}, err + } + + return s.recipeRepository.GetRecipes(ids, &userId) } // GetUserFavoriteRecipes returns a list of the recipes that the user has marked as a // favorite. The user's ID should be provided. Any errors will be bubbled to the caller. -func (s *RecipeService) GetUserFavoriteRecipes(id int) ([]domain.Recipe, error) { - return s.recipeRepository.GetUserFavoriteRecipes(id) +func (s *RecipeService) GetUserFavoriteRecipes(userId int) ([]domain.Recipe, error) { + ids, err := s.recipeRepository.GetUserFavoriteRecipesIds(userId) + if err != nil { + return []domain.Recipe{}, err + } + + return s.recipeRepository.GetRecipes(ids, &userId) } // GetUserViewedRecipes returns a list of the most recent x (limit) recipes viewed by a user, from @@ -211,5 +260,14 @@ func (s *RecipeService) GetUserMadeRecipes(userId, limit int) ([]domain.Recipe, // GetRecipeOfTheWeek searches for the most recent recipe of the week. If there is not a value, // the recipe will be nil. Any errors will be bubbled to the caller. func (s *RecipeService) GetRecipeOfTheWeek(userId *int) (*domain.Recipe, error) { - return s.recipeRepository.GetRecipeOfTheWeek(userId) + id, err := s.recipeRepository.GetRecipeOfTheWeekId(userId) + if err != nil { + return nil, err + } + + if id == nil { + return nil, fmt.Errorf("[ERROR] Recipe of the week ID could not be found. It may not exist.") + } + + return s.recipeRepository.GetRecipe(*id, userId) } diff --git a/internal/domain/recipe/recipe.go b/internal/domain/recipe/recipe.go index bad14a2..52ddbd6 100644 --- a/internal/domain/recipe/recipe.go +++ b/internal/domain/recipe/recipe.go @@ -5,9 +5,9 @@ import "time" // RecipeDuration is the duration to prepare recipe. It has JSON tags which allows it to be // marshaled into a JSON object and stored in the database (JSONB). type RecipeDuration struct { - Total int `json:"total"` - Prep int `json:"prep"` - Cook int `json:"cook"` + Total int `json:"Total"` + Prep int `json:"Prep"` + Cook int `json:"Cook"` } // RecipeMeal is the database enum E_MEAL which defines the meal type of a recipe. Postgres enums @@ -46,11 +46,62 @@ func ParseMeal(meal int) RecipeMeal { } } +// TODO: Comment +type RecipeIngredientUnit string + +const ( + Blank RecipeIngredientUnit = "" + Tsp RecipeIngredientUnit = "tsp" + Tbsp RecipeIngredientUnit = "tbsp" + FlOz RecipeIngredientUnit = "fl oz" + Cup RecipeIngredientUnit = "cup" + Ml RecipeIngredientUnit = "ml" + Litre RecipeIngredientUnit = "l" + Pint RecipeIngredientUnit = "pt" + Quart RecipeIngredientUnit = "qt" + Gallon RecipeIngredientUnit = "gal" + Gram RecipeIngredientUnit = "g" + Kilogram RecipeIngredientUnit = "kg" + Ounce RecipeIngredientUnit = "oz" + Pound RecipeIngredientUnit = "lb" + Piece RecipeIngredientUnit = "piece" + Clove RecipeIngredientUnit = "clove" + Slice RecipeIngredientUnit = "slice" + Stick RecipeIngredientUnit = "stick" + Bunch RecipeIngredientUnit = "bunch" + Pinch RecipeIngredientUnit = "pinch" + Dash RecipeIngredientUnit = "dash" + Splash RecipeIngredientUnit = "splash" + ToTaste RecipeIngredientUnit = "to taste" +) + // RecipeIngredient is a single ingredients in a recipe. These have JSON tags which allow them // to be marshaled into a JSON array and stored in the database (JSONB). type RecipeIngredient struct { - Name string `json:"name"` - Quantity string `json:"quantity"` + Id string `json:"Id"` + SectionId string `json:"SectionId"` + Name string `json:"Name"` + Amount float64 `json:"Amount"` + Unit RecipeIngredientUnit `json:"Unit"` +} + +// TODO: Comment +type RecipeInstruction struct { + Id string `json:"Id"` + Content string `json:"Content"` +} + +// TODO: Comment +type RecipeIngredientSection struct { + Id string `json:"Id"` + Name string `json:"Name"` +} + +// RecipeIngredientStore is the struct stored in the database Ingredients column. It is simply a +// combindation of the sections and the ingredients so they can be stored together. +type RecipeIngredientStore struct { + Sections []RecipeIngredientSection `json:"Sections"` + Ingredients []RecipeIngredient `json:"Ingredients"` } // Recipe is the database model of a recipe. There is no need to map to a different model so @@ -61,12 +112,13 @@ type Recipe struct { Id int Title string Description string - Instructions []string + Instructions []RecipeInstruction Serves int Difficulty int Duration RecipeDuration Category RecipeMeal - Ingredients []RecipeIngredient // Just a list of ingredients + Ingredients []RecipeIngredient + Sections []RecipeIngredientSection UserId int Modified *time.Time // Pointer to allow null Created time.Time @@ -78,11 +130,12 @@ type Recipe struct { // The integer values should be provided as bits and used to parse out individual flags. More // details can be found in the SearchRecipes service function. type SearchFilters struct { - Search string - MealType int - Time int - Difficulty int - ServingSize int + Search string `json:"Search"` + MealType int `json:"MealType"` + Time int `json:"Time"` + Difficulty int `json:"Difficulty"` + ServingSize int `json:"ServingSize"` + Favorites bool `json:"Favorites"` } // Tag is a model which represents a single tag in the Tags table. A tag is mapped to a recipe @@ -101,3 +154,17 @@ type RecipeTag struct { TagId int Created time.Time } + +// TODO: Comment +type CreateRecipeRequest struct { + Title string + Description string + Instructions []RecipeInstruction + Serves int + Difficulty int + Duration RecipeDuration + Category RecipeMeal + Ingredients []RecipeIngredient + Sections []RecipeIngredientSection + Tags []string +} diff --git a/internal/domain/recipe/repository.go b/internal/domain/recipe/repository.go index 8574035..93de1f4 100644 --- a/internal/domain/recipe/repository.go +++ b/internal/domain/recipe/repository.go @@ -4,11 +4,11 @@ type RecipeRepository interface { CreateRecipe(recipe *Recipe) error GetRecipe(id int, userId *int) (*Recipe, error) GetRecipes(ids []int, userId *int) ([]Recipe, error) - SearchRecipes(filters SearchFilters, userId *int, favorites bool) ([]Recipe, error) + SearchRecipes(filters SearchFilters, userId *int, favorites bool) ([]int, error) CreateRecipeTags(recipe Recipe, tags []string) error - GetUserRecipes(id int) ([]Recipe, error) - GetUserFavoriteRecipes(id int) ([]Recipe, error) + GetUserRecipesIds(userId int) ([]int, error) + GetUserFavoriteRecipesIds(userId int) ([]int, error) GetRecipeTags(recipe *Recipe) error GetRecipeFavorite(recipe *Recipe, userId int) error - GetRecipeOfTheWeek(userId *int) (*Recipe, error) + GetRecipeOfTheWeekId(userId *int) (*int, error) } diff --git a/internal/domain/recipe/service.go b/internal/domain/recipe/service.go index 91bf12f..9c6a75f 100644 --- a/internal/domain/recipe/service.go +++ b/internal/domain/recipe/service.go @@ -8,8 +8,8 @@ type RecipeService interface { CreateRecipe(ctx *gin.Context) (*Recipe, error) GetRecipe(id int, userId *int) (*Recipe, error) SearchRecipes(filters SearchFilters, userId *int, favorites bool) ([]Recipe, error) - GetUserRecipes(id int) ([]Recipe, error) - GetUserFavoriteRecipes(id int) ([]Recipe, error) + GetUserRecipes(userId int) ([]Recipe, error) + GetUserFavoriteRecipes(userId int) ([]Recipe, error) GetUserViewedRecipes(userId, limit int) ([]Recipe, error) GetUserMadeRecipes(userId, limit int) ([]Recipe, error) GetRecipeOfTheWeek(userId *int) (*Recipe, error) diff --git a/internal/domain/server/routes.go b/internal/domain/server/routes.go index 19053ce..c65f1af 100644 --- a/internal/domain/server/routes.go +++ b/internal/domain/server/routes.go @@ -1,36 +1,38 @@ package domain // Sub-routes -const VERSION = "/v1" +const VERSION_1 = "/v1" +const VERSION_2 = "/v2" const WEB = "/web" const API = "/api" const STATE = "/state" // Web prefixed routes -const WEB_LOGIN = VERSION + WEB + "/login" -const WEB_INDEX = VERSION + WEB -const WEB_HOME = VERSION + WEB + "/home" -const WEB_FAVORITES = VERSION + WEB + "/favorites" -const WEB_CREATE = VERSION + WEB + "/create" -const WEB_PROFIlE = VERSION + WEB + "/profile" -const WEB_LIST = VERSION + WEB + "/list" -const WEB_RECIPE = VERSION + WEB + "/recipe/%d" -const WEB_SEARCH = VERSION + WEB + "/search" -const WEB_NOT_FOUND = VERSION + WEB + "/404" +const WEB_LOGIN = VERSION_1 + WEB + "/login" +const WEB_INDEX = VERSION_1 + WEB +const WEB_HOME = VERSION_1 + WEB + "/home" +const WEB_FAVORITES = VERSION_1 + WEB + "/favorites" +const WEB_CREATE = VERSION_1 + WEB + "/create" +const WEB_PROFIlE = VERSION_1 + WEB + "/profile" +const WEB_LIST = VERSION_1 + WEB + "/list" +const WEB_RECIPE = VERSION_1 + WEB + "/recipe/%d" +const WEB_SEARCH = VERSION_1 + WEB + "/search" +const WEB_NOT_FOUND = VERSION_1 + WEB + "/404" // API prefixed routes -const API_AUTH_LOGIN = VERSION + API + "/auth/login" -const API_AUTH_CALLBACK = VERSION + API + "/auth/callback" -const API_AUTH_LOGOUT = VERSION + API + "/auth/logout" -const API_CREATE_RECIPE = VERSION + API + "/recipe" -const API_SEARCH_RECIPES = VERSION + API + "/recipe/search" -const API_SEARCH_FAVORITES = VERSION + API + "/recipe/search/favorites" +const API_AUTH_LOGIN = VERSION_1 + API + "/auth/login" +const API_AUTH_CALLBACK = VERSION_1 + API + "/auth/callback" +const API_AUTH_CALLBACK_V2 = VERSION_2 + API + "/auth/callback" +const API_AUTH_LOGOUT = VERSION_1 + API + "/auth/logout" +const API_CREATE_RECIPE = VERSION_1 + API + "/recipe" +const API_SEARCH_RECIPES = VERSION_1 + API + "/recipe/search" +const API_SEARCH_FAVORITES = VERSION_1 + API + "/recipe/search/favorites" -const API_ENGAGEMENT_VIEW = VERSION + API + "/engagement/view/%d" -const API_ENGAGEMENT_SHARE = VERSION + API + "/engagement/share/%d" -const API_ENGAGEMENT_FAVORITE = VERSION + API + "/engagement/favorite/%d" -const API_ENGAGEMENT_MAKE = VERSION + API + "/engagement/make/%d" +const API_ENGAGEMENT_VIEW = VERSION_1 + API + "/engagement/view/%d" +const API_ENGAGEMENT_SHARE = VERSION_1 + API + "/engagement/share/%d" +const API_ENGAGEMENT_FAVORITE = VERSION_1 + API + "/engagement/favorite/%d" +const API_ENGAGEMENT_MAKE = VERSION_1 + API + "/engagement/make/%d" // State prefixed routes -const STATE_TAGS_CREATE = VERSION + WEB + STATE + "/tags" -const STATE_TAGS_DELETE = VERSION + WEB + STATE + "/tags/delete" +const STATE_TAGS_CREATE = VERSION_1 + WEB + STATE + "/tags" +const STATE_TAGS_DELETE = VERSION_1 + WEB + STATE + "/tags/delete" diff --git a/internal/domain/server/server.go b/internal/domain/server/server.go index da17824..7afc797 100644 --- a/internal/domain/server/server.go +++ b/internal/domain/server/server.go @@ -23,6 +23,7 @@ type EnvironmentConfig struct { DatabaseUrl string Environment string Domain string + FrontendDomain string } // InjectedDependencies is a collection of dependencies that are injected into the application. They @@ -38,8 +39,8 @@ type InjectedDependencies struct { // JwtClaims is the data stored in the JSON web token. All that is needed is the users ID and their // Google email provided. type JwtClaims struct { - UserId int `json:"id"` - Email string `json:"email"` + UserId int `json:"Id"` + Email string `json:"Email"` jwt.RegisteredClaims } @@ -81,16 +82,25 @@ func LoadEnvironment() (*EnvironmentConfig, error) { } var domain string + var frontendDomain string if env == "dev" { domain = os.Getenv("DOMAIN_DEV") if domain == "" { return nil, fmt.Errorf("DOMAIN_DEV environment variable is required when ENVIRONMENT is 'dev'.") } + frontendDomain = os.Getenv("FRONTEND_DOMAIN_DEV") + if frontendDomain == "" { + return nil, fmt.Errorf("FRONTEND_DOMAIN_DEV environment variable is required when ENVIRONMENT is 'dev'.") + } } else if env == "prod" { domain = os.Getenv("DOMAIN_PROD") if domain == "" { return nil, fmt.Errorf("DOMAIN_PROD environment variable is required when ENVIRONMENT is 'prod'.") } + frontendDomain = os.Getenv("FRONTEND_DOMAIN_PROD") + if frontendDomain == "" { + return nil, fmt.Errorf("FRONTEND_DOMAIN_PROD environment variable is required when ENVIRONMENT is 'dev'.") + } } else { return nil, fmt.Errorf("ENVIRONMENT environment variable is required and must be 'dev' or 'prod'.") } @@ -117,8 +127,11 @@ func LoadEnvironment() (*EnvironmentConfig, error) { DatabaseUrl: dbUrl, Environment: env, Domain: domain, + FrontendDomain: frontendDomain, } + fmt.Printf("Environment Config: %+v\n", cfg) + return cfg, nil } diff --git a/internal/domain/user/user.go b/internal/domain/user/user.go index ff744a0..0c3b167 100644 --- a/internal/domain/user/user.go +++ b/internal/domain/user/user.go @@ -4,13 +4,13 @@ import "time" // GoogleUserInfo is a data type which contains a mapping of the Google User Info API call. type GoogleUserInfo struct { - Id string `json:"id"` - Email string `json:"email"` - Verified bool `json:"verified_email"` - Name string `json:"name"` - GivenName string `json:"given_name"` - FamilyName string `json:"family_name"` - Picture string `json:"picture"` + Id string `json:"Id"` + Email string `json:"Email"` + Verified bool `json:"VerifiedEmail"` + Name string `json:"Name"` + GivenName string `json:"GivenName"` + FamilyName string `json:"FamilyName"` + Picture string `json:"Picture"` } // User is the database model of a user. There is no need to map to a different model so diff --git a/internal/infrastructure/database/repository/recipe_repository.go b/internal/infrastructure/database/repository/recipe_repository.go index acc793f..ea8376a 100644 --- a/internal/infrastructure/database/repository/recipe_repository.go +++ b/internal/infrastructure/database/repository/recipe_repository.go @@ -46,7 +46,9 @@ func (r *RecipeRepository) CreateRecipe(recipe *domain.Recipe) error { // NOTE: Data steps // cast duration to JSON - // cast ingredients to JSON + // convert ingredients to store type + // cast store type to JSON + // extract string instructions from type // cast category to string // use nil for the modified time @@ -55,17 +57,27 @@ func (r *RecipeRepository) CreateRecipe(recipe *domain.Recipe) error { return err } - ingredientsJSON, err := json.Marshal(recipe.Ingredients) + ingredientsStore := domain.RecipeIngredientStore{ + Sections: recipe.Sections, + Ingredients: recipe.Ingredients, + } + + ingredientsJSON, err := json.Marshal(ingredientsStore) if err != nil { return err } + instructions := make([]string, len(recipe.Instructions)) + for i, instruction := range recipe.Instructions { + instructions[i] = instruction.Content + } + var id int if err = tx.QueryRow( query, recipe.Title, recipe.Description, - pq.Array(recipe.Instructions), + pq.Array(instructions), recipe.Serves, recipe.Difficulty, durationJSON, @@ -94,8 +106,7 @@ func (r *RecipeRepository) CreateRecipe(recipe *domain.Recipe) error { // for added safety. The repository will not check for a nil result, instead the service will. Callers // are responsible for protecting against double nil results. Any errors will be bubbled to the caller. func (r *RecipeRepository) GetRecipe(id int, userId *int) (*domain.Recipe, error) { - query := ` - SELECT + query := ` SELECT id, title, description, instructions, serves, difficulty, duration, category, ingredients, userid, modified, created FROM recipes @@ -103,6 +114,7 @@ func (r *RecipeRepository) GetRecipe(id int, userId *int) (*domain.Recipe, error ` var durationBytes []byte + var instructions pq.StringArray var ingredientBytes []byte var recipe domain.Recipe @@ -110,7 +122,8 @@ func (r *RecipeRepository) GetRecipe(id int, userId *int) (*domain.Recipe, error &recipe.Id, &recipe.Title, &recipe.Description, - pq.Array(&recipe.Instructions), + // pq.Array(&instructions), + &instructions, &recipe.Serves, &recipe.Difficulty, &durationBytes, @@ -137,16 +150,23 @@ func (r *RecipeRepository) GetRecipe(id int, userId *int) (*domain.Recipe, error // Parse ingredient if len(ingredientBytes) > 0 { - var ingredients []domain.RecipeIngredient - if err := json.Unmarshal(ingredientBytes, &ingredients); err != nil { + var store domain.RecipeIngredientStore + if err := json.Unmarshal(ingredientBytes, &store); err != nil { + // Check for unmarshal to support backwards compatability return nil, fmt.Errorf("Failed to parse ingredients from database: %s", err.Error()) } - recipe.Ingredients = ingredients + recipe.Ingredients = store.Ingredients + recipe.Sections = store.Sections } else { recipe.Ingredients = []domain.RecipeIngredient{} } + // Add instructions + for _, instruction := range instructions { + recipe.Instructions = append(recipe.Instructions, domain.RecipeInstruction{Content: instruction}) + } + // Add tags if err := r.GetRecipeTags(&recipe); err != nil { fmt.Printf("ERROR getting recipe tags. %s\n", err.Error()) @@ -169,83 +189,18 @@ func (r *RecipeRepository) GetRecipe(id int, userId *int) (*domain.Recipe, error // will. Callers are responsible for protecting against double nil results. Any errors will be bubbled // to the caller. func (r *RecipeRepository) GetRecipes(ids []int, userId *int) ([]domain.Recipe, error) { - query := ` - SELECT - id, title, description, instructions, serves, difficulty, duration, category, ingredients, userid, modified, created - FROM recipes - WHERE id = ANY($1) - ORDER BY array_position($1, id); - ` - var recipes []domain.Recipe - rows, err := r.db.Query(query, pq.Array(ids)) - if err != nil { - return nil, fmt.Errorf("Failed to get recipes. %s", err.Error()) - } - defer rows.Close() - - for rows.Next() { - var recipe domain.Recipe - var durationBytes []byte - var ingredientBytes []byte - - if err := rows.Scan( - &recipe.Id, - &recipe.Title, - &recipe.Description, - pq.Array(&recipe.Instructions), - &recipe.Serves, - &recipe.Difficulty, - &durationBytes, - &recipe.Category, - &ingredientBytes, - &recipe.UserId, - &recipe.Modified, - &recipe.Created, - ); err != nil { - return nil, fmt.Errorf("Failed to scan recipe from database: %s", err.Error()) + for _, id := range ids { + recipe, err := r.GetRecipe(id, userId) + if err != nil { + return nil, err } - // Parse duration - if len(durationBytes) > 0 { - var duration domain.RecipeDuration - if err := json.Unmarshal(durationBytes, &duration); err != nil { - return nil, fmt.Errorf("Failed to parse duration from database: %s", err.Error()) - } - - recipe.Duration = duration - } else { - recipe.Duration = domain.RecipeDuration{} + // Skip any un-found recipes...? + if recipe != nil { + recipes = append(recipes, *recipe) } - - // Parse ingredient - if len(ingredientBytes) > 0 { - var ingredients []domain.RecipeIngredient - if err := json.Unmarshal(ingredientBytes, &ingredients); err != nil { - return nil, fmt.Errorf("Failed to parse ingredients from database: %s", err.Error()) - } - - recipe.Ingredients = ingredients - } else { - recipe.Ingredients = []domain.RecipeIngredient{} - } - - // Add tags - if err := r.GetRecipeTags(&recipe); err != nil { - fmt.Printf("ERROR getting recipe tags. %s\n", err.Error()) - } - - // Get favorite status, if user id is provided - if userId != nil { - if err := r.GetRecipeFavorite(&recipe, *userId); err != nil { - fmt.Printf("ERROR getting recipe favorite status. %s\n", err.Error()) - } - } else { - recipe.Favorite = false - } - - recipes = append(recipes, recipe) } return recipes, nil @@ -264,7 +219,12 @@ func isBitActive(bits, pos int) bool { // The favorites parameter is used to only return filters favorited by the userId provided. // // TODO: Pagination is required, to provide infinite scroll. -func (r *RecipeRepository) SearchRecipes(filters domain.SearchFilters, userId *int, favorites bool) ([]domain.Recipe, error) { +// +// TODO: This does not work in the current build, the DB does not return valid values. +// +// 12/28/25: This function has changed, now longer returns the recipes, but their IDs for fetching +// elsewhere. +func (r *RecipeRepository) SearchRecipes(filters domain.SearchFilters, userId *int, favorites bool) ([]int, error) { // Compute meals type filters (there are 7 bits) var mealConditions []string for i := range 7 { @@ -348,17 +308,6 @@ func (r *RecipeRepository) SearchRecipes(filters domain.SearchFilters, userId *i // Define columns to select. More fields can be added if the full text search is required columns := []string{ "r.id", - "r.title", - "r.description", - "r.instructions", - "r.serves", - "r.difficulty", - "r.duration", - "r.category", - "r.ingredients", - "r.userid", - "r.modified", - "r.created", } // TODO: Need to add these to the query @@ -435,76 +384,21 @@ func (r *RecipeRepository) SearchRecipes(filters domain.SearchFilters, userId *i // Execute the query rows, err := r.db.Query(query) if err != nil { - return nil, fmt.Errorf("failed to query recipes: %w", err) + return []int{}, fmt.Errorf("failed to query recipes: %w", err) } defer rows.Close() - var recipes []domain.Recipe + var ids []int for rows.Next() { - // Parsed values location - var recipe domain.Recipe - var durationBytes []byte - var ingredientBytes []byte - - if err := rows.Scan( - &recipe.Id, - &recipe.Title, - &recipe.Description, - pq.Array(&recipe.Instructions), - &recipe.Serves, - &recipe.Difficulty, - &durationBytes, - &recipe.Category, - &ingredientBytes, - &recipe.UserId, - &recipe.Modified, - &recipe.Created, - ); err != nil { - return nil, fmt.Errorf("failed to scan recipe row: %w", err) + var id int + if err := rows.Scan(&id); err != nil { + return []int{}, fmt.Errorf("failed to extract ID: %s\n", err.Error()) } - // Parse duration from bytes - if len(durationBytes) > 0 { - var duration domain.RecipeDuration - if err := json.Unmarshal(durationBytes, &duration); err != nil { - return nil, fmt.Errorf("failed to parse duration for recipe ID %d: %w", recipe.Id, err) - } - recipe.Duration = duration - } else { - recipe.Duration = domain.RecipeDuration{} - } - - // Parse ingredients from bytes - if len(ingredientBytes) > 0 { - var ingredients []domain.RecipeIngredient - if err := json.Unmarshal(ingredientBytes, &ingredients); err != nil { - return nil, fmt.Errorf("failed to parse ingredients for recipe ID %d: %w", recipe.Id, err) - } - recipe.Ingredients = ingredients - } else { - recipe.Ingredients = []domain.RecipeIngredient{} - } - - // Add tags - if err := r.GetRecipeTags(&recipe); err != nil { - fmt.Printf("ERROR getting recipe tags. %s\n", err.Error()) - } - - // Add recipe if not a favorite search - if !favorites && userId != nil { - if err := r.GetRecipeFavorite(&recipe, *userId); err != nil { - fmt.Printf("ERROR getting recipe tags. %s\n", err.Error()) - } - } - - if favorites { - recipe.Favorite = true - } - - recipes = append(recipes, recipe) + ids = append(ids, id) } - return recipes, nil + return ids, nil } // CreateRecipeTags accepts a list of tags (names) and a recipe (already created by the DB) and @@ -569,96 +463,43 @@ func (r *RecipeRepository) CreateRecipeTags(recipe domain.Recipe, tags []string) // GetUserRecipes gets a list of a users owned recipes. This function does not ensure the user is // authenticated or exists. If nothing is found, a blank slice will be returned. The resulting list // is sorted by the created dates, newest first. Any errors will be bubbled to the caller. -func (r *RecipeRepository) GetUserRecipes(id int) ([]domain.Recipe, error) { +// +// 12/28/25: This now returns just the IDs, the service can handle fetching them. +func (r *RecipeRepository) GetUserRecipesIds(user_id int) ([]int, error) { query := ` - SELECT id, title, description, instructions, serves, difficulty, duration, category, ingredients, - userid, modified, created + SELECT id FROM recipes WHERE userid = $1 ORDER BY created DESC; ` - rows, err := r.db.Query(query, id) + rows, err := r.db.Query(query, user_id) if err != nil { return nil, fmt.Errorf("Failed to query DB for user recipes. %s\n", err.Error()) } defer rows.Close() - // Prepare statement for tag query - // tagQuery := ` - // ` - - var recipes []domain.Recipe + var ids []int for rows.Next() { - var recipe domain.Recipe - var durationBytes []byte - var ingredientBytes []byte - - // Scan results from recipe query onto recipe object - if err := rows.Scan( - &recipe.Id, - &recipe.Title, - &recipe.Description, - pq.Array(&recipe.Instructions), - &recipe.Serves, - &recipe.Difficulty, - &durationBytes, - &recipe.Category, - &ingredientBytes, - &recipe.UserId, - &recipe.Modified, - &recipe.Created, - ); err != nil { - return nil, fmt.Errorf("Failed to scan row onto recipe object. %s\n", err.Error()) + var r_id int + if err := rows.Scan(&r_id); err != nil { + return []int{}, fmt.Errorf("Failed to scan ID from db. %s\n", err.Error()) } - // Parse duration - if len(durationBytes) > 0 { - var duration domain.RecipeDuration - if err := json.Unmarshal(durationBytes, &duration); err != nil { - return nil, fmt.Errorf("Failed to parse duration from database: %s", err.Error()) - } - - recipe.Duration = duration - } else { - recipe.Duration = domain.RecipeDuration{} - } - - // Parse ingredient - if len(ingredientBytes) > 0 { - var ingredients []domain.RecipeIngredient - if err := json.Unmarshal(ingredientBytes, &ingredients); err != nil { - return nil, fmt.Errorf("Failed to parse ingredients from database: %s", err.Error()) - } - - recipe.Ingredients = ingredients - } else { - recipe.Ingredients = []domain.RecipeIngredient{} - } - - // Add tags - if err := r.GetRecipeTags(&recipe); err != nil { - fmt.Printf("ERROR getting recipe tags. %s\n", err.Error()) - } - - // Get favorite status - if err := r.GetRecipeFavorite(&recipe, id); err != nil { - fmt.Printf("ERROR getting recipe favorite status. %s\n", err.Error()) - } - - recipes = append(recipes, recipe) + ids = append(ids, r_id) } - return recipes, nil + return ids, nil } // GetUserRecipes gets a list of a users favorited recipes. This function does not ensure the user is // authenticated or exists. If nothing is found, a blank slice will be returned. The resulting list // is sorted by the created dates, newest first. Any errors will be bubbled to the caller. -func (r *RecipeRepository) GetUserFavoriteRecipes(id int) ([]domain.Recipe, error) { +// +// 12/28/25: This now just returns the IDs, so the service can handle the fetching. +func (r *RecipeRepository) GetUserFavoriteRecipesIds(id int) ([]int, error) { query := ` - SELECT r.id, r.title, r.description, r.instructions, r.serves, r.difficulty, r.duration, r.category, r.ingredients, r. - userid, r.modified, r.created + SELECT r.id FROM favorites f JOIN recipes r ON r.id = f.recipeid WHERE f.userid = $1 @@ -670,66 +511,17 @@ func (r *RecipeRepository) GetUserFavoriteRecipes(id int) ([]domain.Recipe, erro } defer rows.Close() - var recipes []domain.Recipe + var ids []int for rows.Next() { - var recipe domain.Recipe - var durationBytes []byte - var ingredientBytes []byte - - // Scan results from recipe query onto recipe object - if err := rows.Scan( - &recipe.Id, - &recipe.Title, - &recipe.Description, - pq.Array(&recipe.Instructions), - &recipe.Serves, - &recipe.Difficulty, - &durationBytes, - &recipe.Category, - &ingredientBytes, - &recipe.UserId, - &recipe.Modified, - &recipe.Created, - ); err != nil { - return nil, fmt.Errorf("Failed to scan row onto recipe object. %s\n", err.Error()) + var r_id int + if err := rows.Scan(&r_id); err != nil { + return []int{}, fmt.Errorf("Failed to scan ID from db. %s\n", err.Error()) } - // Parse duration - if len(durationBytes) > 0 { - var duration domain.RecipeDuration - if err := json.Unmarshal(durationBytes, &duration); err != nil { - return nil, fmt.Errorf("Failed to parse duration from database: %s", err.Error()) - } - - recipe.Duration = duration - } else { - recipe.Duration = domain.RecipeDuration{} - } - - // Parse ingredient - if len(ingredientBytes) > 0 { - var ingredients []domain.RecipeIngredient - if err := json.Unmarshal(ingredientBytes, &ingredients); err != nil { - return nil, fmt.Errorf("Failed to parse ingredients from database: %s", err.Error()) - } - - recipe.Ingredients = ingredients - } else { - recipe.Ingredients = []domain.RecipeIngredient{} - } - - // Add tags - if err := r.GetRecipeTags(&recipe); err != nil { - fmt.Printf("ERROR getting recipe tags. %s\n", err.Error()) - } - - // Set favorite status (they're always true!) - recipe.Favorite = true - - recipes = append(recipes, recipe) + ids = append(ids, r_id) } - return recipes, nil + return ids, nil } // GetRecipeTags requires a recipe to be filled with at least an ID. This function will use the ID @@ -793,82 +585,27 @@ func (r *RecipeRepository) GetRecipeFavorite(recipe *domain.Recipe, userId int) return nil } -// GetRecipeOfTheWeek searches for the most recent recipe of the week. If there is not a value, +// GetRecipeOfTheWeekId searches for the most recent recipe of the week. If there is not a value, // the recipe will be nil. This function simply collects the most recent entry in the recipeoftheweek -// table and return it. If there is no entry, nil will be returned Any errors will be bubbled to -// the caller. -func (r *RecipeRepository) GetRecipeOfTheWeek(userId *int) (*domain.Recipe, error) { +// table and return it. If there is no entry, nil will be returned. Any errors will be bubbled to +// the caller. All that is returned is the recipe ID, that way the caller can handle the fetching. +func (r *RecipeRepository) GetRecipeOfTheWeekId(userId *int) (*int, error) { query := ` SELECT - r.id, r.title, r.description, r.instructions, r.serves, r.difficulty, r.duration, r.category, - r.ingredients, r.userid, r.modified, r.created + r.id FROM recipes r JOIN recipeoftheweek rw ON rw.recipeid = r.id - ORDER BY created DESC + ORDER BY rw.created DESC LIMIT 1; ` - var durationBytes []byte - var ingredientBytes []byte - - var recipe domain.Recipe - if err := r.db.QueryRow(query).Scan( - &recipe.Id, - &recipe.Title, - &recipe.Description, - pq.Array(&recipe.Instructions), - &recipe.Serves, - &recipe.Difficulty, - &durationBytes, - &recipe.Category, - &ingredientBytes, - &recipe.UserId, - &recipe.Modified, - &recipe.Created, - ); err != nil { + var id int + if err := r.db.QueryRow(query).Scan(&id); err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, nil } return nil, fmt.Errorf("Failed to location recipe in database: %s", err.Error()) } - // Parse duration - if len(durationBytes) > 0 { - var duration domain.RecipeDuration - if err := json.Unmarshal(durationBytes, &duration); err != nil { - return nil, fmt.Errorf("Failed to parse duration from database: %s", err.Error()) - } - - recipe.Duration = duration - } else { - recipe.Duration = domain.RecipeDuration{} - } - - // Parse ingredient - if len(ingredientBytes) > 0 { - var ingredients []domain.RecipeIngredient - if err := json.Unmarshal(ingredientBytes, &ingredients); err != nil { - return nil, fmt.Errorf("Failed to parse ingredients from database: %s", err.Error()) - } - - recipe.Ingredients = ingredients - } else { - recipe.Ingredients = []domain.RecipeIngredient{} - } - - // Add tags - if err := r.GetRecipeTags(&recipe); err != nil { - fmt.Printf("ERROR getting recipe tags. %s\n", err.Error()) - } - - // Get favorite status, if user id is provided - if userId != nil { - if err := r.GetRecipeFavorite(&recipe, *userId); err != nil { - fmt.Printf("ERROR getting recipe favorite status. %s\n", err.Error()) - } - } else { - recipe.Favorite = false - } - - return &recipe, nil + return &id, nil } diff --git a/internal/templates/components/banner_templ.go b/internal/templates/components/banner_templ.go index 8d5dc82..f4a8c3e 100644 --- a/internal/templates/components/banner_templ.go +++ b/internal/templates/components/banner_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.943 +// templ: version: v0.3.960 package components //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/internal/templates/components/cards_templ.go b/internal/templates/components/cards_templ.go index 4fcd054..0f6f16c 100644 --- a/internal/templates/components/cards_templ.go +++ b/internal/templates/components/cards_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.943 +// templ: version: v0.3.960 package components //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/internal/templates/components/dropdowns_templ.go b/internal/templates/components/dropdowns_templ.go index 5f39d15..d749993 100644 --- a/internal/templates/components/dropdowns_templ.go +++ b/internal/templates/components/dropdowns_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.943 +// templ: version: v0.3.960 package components //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/internal/templates/components/error_templ.go b/internal/templates/components/error_templ.go index 1bfb31b..5055698 100644 --- a/internal/templates/components/error_templ.go +++ b/internal/templates/components/error_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.943 +// templ: version: v0.3.960 package components //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/internal/templates/components/navbar_templ.go b/internal/templates/components/navbar_templ.go index ba562fd..fde40cf 100644 --- a/internal/templates/components/navbar_templ.go +++ b/internal/templates/components/navbar_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.943 +// templ: version: v0.3.960 package components //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/internal/templates/components/search_bar_templ.go b/internal/templates/components/search_bar_templ.go index 2e78e1e..92bbc52 100644 --- a/internal/templates/components/search_bar_templ.go +++ b/internal/templates/components/search_bar_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.943 +// templ: version: v0.3.960 package components //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/internal/templates/layouts/app_layout_templ.go b/internal/templates/layouts/app_layout_templ.go index 0608ea8..f2fc957 100644 --- a/internal/templates/layouts/app_layout_templ.go +++ b/internal/templates/layouts/app_layout_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.943 +// templ: version: v0.3.960 package templates //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/internal/templates/pages/create_templ.go b/internal/templates/pages/create_templ.go index 28b33db..30930d8 100644 --- a/internal/templates/pages/create_templ.go +++ b/internal/templates/pages/create_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.943 +// templ: version: v0.3.960 package templates //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/internal/templates/pages/favorites_templ.go b/internal/templates/pages/favorites_templ.go index e888fc2..0d4c599 100644 --- a/internal/templates/pages/favorites_templ.go +++ b/internal/templates/pages/favorites_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.943 +// templ: version: v0.3.960 package templates //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/internal/templates/pages/home_templ.go b/internal/templates/pages/home_templ.go index 447abab..26d06ee 100644 --- a/internal/templates/pages/home_templ.go +++ b/internal/templates/pages/home_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.943 +// templ: version: v0.3.960 package templates //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/internal/templates/pages/list_templ.go b/internal/templates/pages/list_templ.go index 4a91ce0..e10a7d1 100644 --- a/internal/templates/pages/list_templ.go +++ b/internal/templates/pages/list_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.943 +// templ: version: v0.3.960 package templates //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/internal/templates/pages/login_templ.go b/internal/templates/pages/login_templ.go index 1cb6d29..de877a1 100644 --- a/internal/templates/pages/login_templ.go +++ b/internal/templates/pages/login_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.943 +// templ: version: v0.3.960 package templates //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/internal/templates/pages/notFound_templ.go b/internal/templates/pages/notFound_templ.go index 9ebcde1..59f71ec 100644 --- a/internal/templates/pages/notFound_templ.go +++ b/internal/templates/pages/notFound_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.943 +// templ: version: v0.3.960 package templates //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/internal/templates/pages/profile_templ.go b/internal/templates/pages/profile_templ.go index a99005c..afdf4e3 100644 --- a/internal/templates/pages/profile_templ.go +++ b/internal/templates/pages/profile_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.943 +// templ: version: v0.3.960 package templates //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/internal/templates/pages/recipe.templ b/internal/templates/pages/recipe.templ index 69d6c02..5009f9d 100644 --- a/internal/templates/pages/recipe.templ +++ b/internal/templates/pages/recipe.templ @@ -112,7 +112,7 @@ templ ingredientList(ingredients []domain.RecipeIngredient) {
    @@ -308,7 +308,7 @@ templ RecipePage(recipe domain.Recipe, user domainUser.User, loggedIn bool, doma

    { recipe.Description }

    @ingredientList(recipe.Ingredients) - @instructionList(recipe.Instructions) + @instructionList([]string{}) @tagList(recipe.Tags, recipe.Created, recipe.Modified) diff --git a/internal/templates/pages/recipe_templ.go b/internal/templates/pages/recipe_templ.go index 7a3ba58..d1a3b13 100644 --- a/internal/templates/pages/recipe_templ.go +++ b/internal/templates/pages/recipe_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.943 +// templ: version: v0.3.960 package templates //lint:file-ignore SA4006 This context is only used if a nested component is present. @@ -268,7 +268,7 @@ func ingredientList(ingredients []domain.RecipeIngredient) templ.Component { return templ_7745c5c3_Err } for _, ingredient := range ingredients { - templ_7745c5c3_Err = ingredientListItem(ingredient.Name, ingredient.Quantity).Render(ctx, templ_7745c5c3_Buffer) + templ_7745c5c3_Err = ingredientListItem(ingredient.Name, "").Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -875,7 +875,7 @@ func RecipePage(recipe domain.Recipe, user domainUser.User, loggedIn bool, domai if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = instructionList(recipe.Instructions).Render(ctx, templ_7745c5c3_Buffer) + templ_7745c5c3_Err = instructionList([]string{}).Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/internal/templates/pages/search_templ.go b/internal/templates/pages/search_templ.go index 2e7d66b..f90dba4 100644 --- a/internal/templates/pages/search_templ.go +++ b/internal/templates/pages/search_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.943 +// templ: version: v0.3.960 package templates //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..b51a1fa --- /dev/null +++ b/shell.nix @@ -0,0 +1,34 @@ +{ pkgs ? import {} }: + +pkgs.mkShell { + buildInputs = with pkgs; [ + go + gopls + go-tools + htmx-lsp2 + templ + tailwindcss_4 + tailwindcss-language-server + watchman + docker-language-server + dockerfile-language-server-nodejs + gcc_multi + glibc_multi + nodejs + ]; + + shellHook = '' + alias vim="nvim" + alias vi="nvim" + alias v="nvim" + + # Modify this + export PS1="\[\e[35m\]\w \$ \[\e[0m\]" + + echo "" + echo "The default environment is ready!" + echo "" + + exec zsh + ''; +} diff --git a/web/.gitignore b/web/.gitignore new file mode 100644 index 0000000..50c8dda --- /dev/null +++ b/web/.gitignore @@ -0,0 +1,26 @@ +# 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? + +.env diff --git a/web/Dockerfile b/web/Dockerfile new file mode 100644 index 0000000..99fc94d --- /dev/null +++ b/web/Dockerfile @@ -0,0 +1,32 @@ +# Build stage +FROM node:18-alpine AS build +WORKDIR /app + +COPY package*.json ./ +RUN npm install + +COPY . . + +# Build-time config: defaults are prod-safe, can be overridden if needed +ARG VITE_ENVIRONMENT=prod +ARG VITE_DOMAIN_DEV=http://localhost:3000 +ARG VITE_DOMAIN_PROD=https://potion-backend.gophernest.net + +ENV VITE_ENVIRONMENT=$VITE_ENVIRONMENT +ENV VITE_DOMAIN_DEV=$VITE_DOMAIN_DEV +ENV VITE_DOMAIN_PROD=$VITE_DOMAIN_PROD + +RUN npm run build + +# Runtime stage +FROM node:18-alpine +WORKDIR /app + +# Install static file server +RUN npm install -g serve + +# Copy build output only +COPY --from=build /app/dist ./dist + +EXPOSE 3002 +CMD ["serve", "-s", "dist", "-l", "3002"] diff --git a/web/README.md b/web/README.md new file mode 100644 index 0000000..cb7e169 --- /dev/null +++ b/web/README.md @@ -0,0 +1,4 @@ + +# IF BACKEND CANNOT GET COOKIE + +Do not forget to send the axios request with the `{ withCredentials: true }` flags. diff --git a/web/eslint.config.js b/web/eslint.config.js new file mode 100644 index 0000000..a1279f8 --- /dev/null +++ b/web/eslint.config.js @@ -0,0 +1,43 @@ +import js from "@eslint/js" +import globals from "globals" +import reactHooks from "eslint-plugin-react-hooks" +import reactRefresh from "eslint-plugin-react-refresh" +import tseslint from "typescript-eslint" +import { defineConfig, globalIgnores } from "eslint/config" + +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default defineConfig([ + globalIgnores(["dist"]), + { + files: ["**/*.{ts,tsx}"], + extends: [ + js.configs.recommended, + reactHooks.configs["recommended-latest"], + reactRefresh.configs.vite, + + // Remove tseslint.configs.recommended and replace with this + tseslint.configs.recommendedTypeChecked, + + // Alternatively, use this for stricter rules + // tseslint.configs.strictTypeChecked, + + // Optionally, add this for stylistic rules + tseslint.configs.stylisticTypeChecked, + + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ["./tsconfig.node.json", "./tsconfig.app.json"], + tsconfigRootDir: import.meta.dirname, + }, + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +]) diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..567acc0 --- /dev/null +++ b/web/index.html @@ -0,0 +1,16 @@ + + + + + + + + Gophernest - Potion + + + +
    + + + + diff --git a/web/package-lock.json b/web/package-lock.json new file mode 100644 index 0000000..6df06f1 --- /dev/null +++ b/web/package-lock.json @@ -0,0 +1,4244 @@ +{ + "name": "web", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "web", + "version": "0.0.0", + "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@tailwindcss/vite": "^4.1.16", + "axios": "^1.13.2", + "eslint-plugin-react-dom": "^2.2.4", + "eslint-plugin-react-x": "^2.2.4", + "motion": "^12.23.25", + "react": "^19.1.1", + "react-cookie": "^8.0.1", + "react-dom": "^19.1.1", + "react-router-dom": "^7.9.5", + "tailwindcss": "^4.1.16" + }, + "devDependencies": { + "@eslint/js": "^9.36.0", + "@types/node": "^24.6.0", + "@types/react": "^19.1.16", + "@types/react-dom": "^19.1.9", + "@vitejs/plugin-react-swc": "^4.1.0", + "eslint": "^9.36.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.22", + "globals": "^16.4.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.45.0", + "vite": "^7.1.7" + } + }, + "node_modules/@dnd-kit/accessibility": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", + "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/core": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", + "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", + "license": "MIT", + "dependencies": { + "@dnd-kit/accessibility": "^3.1.1", + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/utilities": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", + "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz", + "integrity": "sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.11.tgz", + "integrity": "sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.11.tgz", + "integrity": "sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.11.tgz", + "integrity": "sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.11.tgz", + "integrity": "sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.11.tgz", + "integrity": "sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.11.tgz", + "integrity": "sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.11.tgz", + "integrity": "sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.11.tgz", + "integrity": "sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.11.tgz", + "integrity": "sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.11.tgz", + "integrity": "sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.11.tgz", + "integrity": "sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.11.tgz", + "integrity": "sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.11.tgz", + "integrity": "sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.11.tgz", + "integrity": "sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.11.tgz", + "integrity": "sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.11.tgz", + "integrity": "sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.11.tgz", + "integrity": "sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.11.tgz", + "integrity": "sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.11.tgz", + "integrity": "sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.11.tgz", + "integrity": "sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.11.tgz", + "integrity": "sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.11.tgz", + "integrity": "sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.11.tgz", + "integrity": "sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.11.tgz", + "integrity": "sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.11.tgz", + "integrity": "sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint-react/ast": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@eslint-react/ast/-/ast-2.2.4.tgz", + "integrity": "sha512-kdG9yMJ2QpEbVPfgvlWqTUAF2L7dZYBAaF8/LPrjDIKB1pCbygxXUoRBPVthxTsE2XTqiLbhPqcWGnI8Q3UNTQ==", + "license": "MIT", + "dependencies": { + "@eslint-react/eff": "2.2.4", + "@typescript-eslint/types": "^8.46.2", + "@typescript-eslint/typescript-estree": "^8.46.2", + "@typescript-eslint/utils": "^8.46.2", + "string-ts": "^2.2.1" + }, + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@eslint-react/core": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@eslint-react/core/-/core-2.2.4.tgz", + "integrity": "sha512-uEfUX2GoIymsBbWccJGpuyz8KCtxyNBxJb2FMyqE37nLtNVPbNsFTHRr6uX1WwkBxw+bUOYDbVDy9zFVbmAJXA==", + "license": "MIT", + "dependencies": { + "@eslint-react/ast": "2.2.4", + "@eslint-react/eff": "2.2.4", + "@eslint-react/shared": "2.2.4", + "@eslint-react/var": "2.2.4", + "@typescript-eslint/scope-manager": "^8.46.2", + "@typescript-eslint/types": "^8.46.2", + "@typescript-eslint/utils": "^8.46.2", + "birecord": "^0.1.1", + "ts-pattern": "^5.9.0" + }, + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@eslint-react/eff": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@eslint-react/eff/-/eff-2.2.4.tgz", + "integrity": "sha512-I26FQr5IEjJDXlcuyL1h/shmUdyyAXZrG+Op/E0Lc6cpGvXg5hn1ptcdKJ23o8BAxq2UY2gwyltGxE2t4ixoJQ==", + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@eslint-react/shared": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@eslint-react/shared/-/shared-2.2.4.tgz", + "integrity": "sha512-jDL17njTyVj/cmveNThHtLLJpHqLRd/z76q+38Zcq+kiA3DfZ8mXyy+EYV4lLwD9dvg1FOMomHBTgV/woqWsRQ==", + "license": "MIT", + "dependencies": { + "@eslint-react/eff": "2.2.4", + "@typescript-eslint/utils": "^8.46.2", + "ts-pattern": "^5.9.0", + "zod": "^4.1.12" + }, + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@eslint-react/var": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@eslint-react/var/-/var-2.2.4.tgz", + "integrity": "sha512-MBh64lfHI6Cr2qjaYlJx7x3FcYqgGK9SSB5/7weRsxv63ZfGiJY+aRi0ahSGsE2JhM0/OhWu0T6T1z4nnEbQxA==", + "license": "MIT", + "dependencies": { + "@eslint-react/ast": "2.2.4", + "@eslint-react/eff": "2.2.4", + "@typescript-eslint/scope-manager": "^8.46.2", + "@typescript-eslint/types": "^8.46.2", + "@typescript-eslint/utils": "^8.46.2", + "ts-pattern": "^5.9.0" + }, + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers/node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz", + "integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==", + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.38.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.38.0.tgz", + "integrity": "sha512-UZ1VpFvXf9J06YG9xQBdnzU+kthors6KjhMAl6f4gH4usHyh31rUf2DLGInT8RFYIReYXNSydgPY0V2LuWgl7A==", + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "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==", + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.43", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.43.tgz", + "integrity": "sha512-5Uxg7fQUCmfhax7FJke2+8B6cqgeUJUD9o2uXIKXhD+mG0mL6NObmVoi9wXEU1tY89mZKgAYA6fTbftx3q2ZPQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.5.tgz", + "integrity": "sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.5.tgz", + "integrity": "sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.5.tgz", + "integrity": "sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.5.tgz", + "integrity": "sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.5.tgz", + "integrity": "sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.5.tgz", + "integrity": "sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.5.tgz", + "integrity": "sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.5.tgz", + "integrity": "sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.5.tgz", + "integrity": "sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.5.tgz", + "integrity": "sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.5.tgz", + "integrity": "sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.5.tgz", + "integrity": "sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.5.tgz", + "integrity": "sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.5.tgz", + "integrity": "sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.5.tgz", + "integrity": "sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.5.tgz", + "integrity": "sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.5.tgz", + "integrity": "sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.5.tgz", + "integrity": "sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.5.tgz", + "integrity": "sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.5.tgz", + "integrity": "sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.5.tgz", + "integrity": "sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.5.tgz", + "integrity": "sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@swc/core": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.14.0.tgz", + "integrity": "sha512-oExhY90bes5pDTVrei0xlMVosTxwd/NMafIpqsC4dMbRYZ5KB981l/CX8tMnGsagTplj/RcG9BeRYmV6/J5m3w==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.25" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.14.0", + "@swc/core-darwin-x64": "1.14.0", + "@swc/core-linux-arm-gnueabihf": "1.14.0", + "@swc/core-linux-arm64-gnu": "1.14.0", + "@swc/core-linux-arm64-musl": "1.14.0", + "@swc/core-linux-x64-gnu": "1.14.0", + "@swc/core-linux-x64-musl": "1.14.0", + "@swc/core-win32-arm64-msvc": "1.14.0", + "@swc/core-win32-ia32-msvc": "1.14.0", + "@swc/core-win32-x64-msvc": "1.14.0" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.14.0.tgz", + "integrity": "sha512-uHPC8rlCt04nvYNczWzKVdgnRhxCa3ndKTBBbBpResOZsRmiwRAvByIGh599j+Oo6Z5eyTPrgY+XfJzVmXnN7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.14.0.tgz", + "integrity": "sha512-2SHrlpl68vtePRknv9shvM9YKKg7B9T13tcTg9aFCwR318QTYo+FzsKGmQSv9ox/Ua0Q2/5y2BNjieffJoo4nA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.14.0.tgz", + "integrity": "sha512-SMH8zn01dxt809svetnxpeg/jWdpi6dqHKO3Eb11u4OzU2PK7I5uKS6gf2hx5LlTbcJMFKULZiVwjlQLe8eqtg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.14.0.tgz", + "integrity": "sha512-q2JRu2D8LVqGeHkmpVCljVNltG0tB4o4eYg+dElFwCS8l2Mnt9qurMCxIeo9mgoqz0ax+k7jWtIRHktnVCbjvQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.14.0.tgz", + "integrity": "sha512-uofpVoPCEUjYIv454ZEZ3sLgMD17nIwlz2z7bsn7rl301Kt/01umFA7MscUovFfAK2IRGck6XB+uulMu6aFhKQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.14.0.tgz", + "integrity": "sha512-quTTx1Olm05fBfv66DEBuOsOgqdypnZ/1Bh3yGXWY7ANLFeeRpCDZpljD9BSjdsNdPOlwJmEUZXMHtGm3v1TZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.14.0.tgz", + "integrity": "sha512-caaNAu+aIqT8seLtCf08i8C3/UC5ttQujUjejhMcuS1/LoCKtNiUs4VekJd2UGt+pyuuSrQ6dKl8CbCfWvWeXw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.14.0.tgz", + "integrity": "sha512-EeW3jFlT3YNckJ6V/JnTfGcX7UHGyh6/AiCPopZ1HNaGiXVCKHPpVQZicmtyr/UpqxCXLrTgjHOvyMke7YN26A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.14.0.tgz", + "integrity": "sha512-dPai3KUIcihV5hfoO4QNQF5HAaw8+2bT7dvi8E5zLtecW2SfL3mUZipzampXq5FHll0RSCLzlrXnSx+dBRZIIQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.14.0.tgz", + "integrity": "sha512-nm+JajGrTqUA6sEHdghDlHMNfH1WKSiuvljhdmBACW4ta4LC3gKurX2qZuiBARvPkephW9V/i5S8QPY1PzFEqg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@swc/types": { + "version": "0.1.25", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.25.tgz", + "integrity": "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.16.tgz", + "integrity": "sha512-BX5iaSsloNuvKNHRN3k2RcCuTEgASTo77mofW0vmeHkfrDWaoFAFvNHpEgtu0eqyypcyiBkDWzSMxJhp3AUVcw==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.6.1", + "lightningcss": "1.30.2", + "magic-string": "^0.30.19", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.16" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.16.tgz", + "integrity": "sha512-2OSv52FRuhdlgyOQqgtQHuCgXnS8nFSYRp2tJ+4WZXKgTxqPy7SMSls8c3mPT5pkZ17SBToGM5LHEJBO7miEdg==", + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.16", + "@tailwindcss/oxide-darwin-arm64": "4.1.16", + "@tailwindcss/oxide-darwin-x64": "4.1.16", + "@tailwindcss/oxide-freebsd-x64": "4.1.16", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.16", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.16", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.16", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.16", + "@tailwindcss/oxide-linux-x64-musl": "4.1.16", + "@tailwindcss/oxide-wasm32-wasi": "4.1.16", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.16", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.16" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.16.tgz", + "integrity": "sha512-8+ctzkjHgwDJ5caq9IqRSgsP70xhdhJvm+oueS/yhD5ixLhqTw9fSL1OurzMUhBwE5zK26FXLCz2f/RtkISqHA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.16.tgz", + "integrity": "sha512-C3oZy5042v2FOALBZtY0JTDnGNdS6w7DxL/odvSny17ORUnaRKhyTse8xYi3yKGyfnTUOdavRCdmc8QqJYwFKA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.16.tgz", + "integrity": "sha512-vjrl/1Ub9+JwU6BP0emgipGjowzYZMjbWCDqwA2Z4vCa+HBSpP4v6U2ddejcHsolsYxwL5r4bPNoamlV0xDdLg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.16.tgz", + "integrity": "sha512-TSMpPYpQLm+aR1wW5rKuUuEruc/oOX3C7H0BTnPDn7W/eMw8W+MRMpiypKMkXZfwH8wqPIRKppuZoedTtNj2tg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.16.tgz", + "integrity": "sha512-p0GGfRg/w0sdsFKBjMYvvKIiKy/LNWLWgV/plR4lUgrsxFAoQBFrXkZ4C0w8IOXfslB9vHK/JGASWD2IefIpvw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.16.tgz", + "integrity": "sha512-DoixyMmTNO19rwRPdqviTrG1rYzpxgyYJl8RgQvdAQUzxC1ToLRqtNJpU/ATURSKgIg6uerPw2feW0aS8SNr/w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.16.tgz", + "integrity": "sha512-H81UXMa9hJhWhaAUca6bU2wm5RRFpuHImrwXBUvPbYb+3jo32I9VIwpOX6hms0fPmA6f2pGVlybO6qU8pF4fzQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.16.tgz", + "integrity": "sha512-ZGHQxDtFC2/ruo7t99Qo2TTIvOERULPl5l0K1g0oK6b5PGqjYMga+FcY1wIUnrUxY56h28FxybtDEla+ICOyew==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.16.tgz", + "integrity": "sha512-Oi1tAaa0rcKf1Og9MzKeINZzMLPbhxvm7rno5/zuP1WYmpiG0bEHq4AcRUiG2165/WUzvxkW4XDYCscZWbTLZw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.16.tgz", + "integrity": "sha512-B01u/b8LteGRwucIBmCQ07FVXLzImWESAIMcUU6nvFt/tYsQ6IHz8DmZ5KtvmwxD+iTYBtM1xwoGXswnlu9v0Q==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.5.0", + "@emnapi/runtime": "^1.5.0", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.0.7", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.16.tgz", + "integrity": "sha512-zX+Q8sSkGj6HKRTMJXuPvOcP8XfYON24zJBRPlszcH1Np7xuHXhWn8qfFjIujVzvH3BHU+16jBXwgpl20i+v9A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.16.tgz", + "integrity": "sha512-m5dDFJUEejbFqP+UXVstd4W/wnxA4F61q8SoL+mqTypId2T2ZpuxosNSgowiCnLp2+Z+rivdU0AqpfgiD7yCBg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.16.tgz", + "integrity": "sha512-bbguNBcDxsRmi9nnlWJxhfDWamY3lmcyACHcdO1crxfzuLpOhHLLtEIN/nCbbAtj5rchUgQD17QVAKi1f7IsKg==", + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.1.16", + "@tailwindcss/oxide": "4.1.16", + "tailwindcss": "4.1.16" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/hoist-non-react-statics": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.7.tgz", + "integrity": "sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g==", + "license": "MIT", + "dependencies": { + "hoist-non-react-statics": "^3.3.0" + }, + "peerDependencies": { + "@types/react": "*" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.9.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.2.tgz", + "integrity": "sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.2", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", + "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", + "license": "MIT", + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.2", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.2.tgz", + "integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.2.tgz", + "integrity": "sha512-ZGBMToy857/NIPaaCucIUQgqueOiq7HeAKkhlvqVV4lm089zUFW6ikRySx2v+cAhKeUCPuWVHeimyk6Dw1iY3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.46.2", + "@typescript-eslint/type-utils": "8.46.2", + "@typescript-eslint/utils": "8.46.2", + "@typescript-eslint/visitor-keys": "8.46.2", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.46.2", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.2.tgz", + "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.46.2", + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/typescript-estree": "8.46.2", + "@typescript-eslint/visitor-keys": "8.46.2", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.2.tgz", + "integrity": "sha512-PULOLZ9iqwI7hXcmL4fVfIsBi6AN9YxRc0frbvmg8f+4hQAjQ5GYNKK0DIArNo+rOKmR/iBYwkpBmnIwin4wBg==", + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.46.2", + "@typescript-eslint/types": "^8.46.2", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.2.tgz", + "integrity": "sha512-LF4b/NmGvdWEHD2H4MsHD8ny6JpiVNDzrSZr3CsckEgCbAGZbYM4Cqxvi9L+WqDMT+51Ozy7lt2M+d0JLEuBqA==", + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/visitor-keys": "8.46.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.2.tgz", + "integrity": "sha512-a7QH6fw4S57+F5y2FIxxSDyi5M4UfGF+Jl1bCGd7+L4KsaUY80GsiF/t0UoRFDHAguKlBaACWJRmdrc6Xfkkag==", + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.2.tgz", + "integrity": "sha512-HbPM4LbaAAt/DjxXaG9yiS9brOOz6fabal4uvUmaUYe6l3K1phQDMQKBRUrr06BQkxkvIZVVHttqiybM9nJsLA==", + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/typescript-estree": "8.46.2", + "@typescript-eslint/utils": "8.46.2", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.2.tgz", + "integrity": "sha512-lNCWCbq7rpg7qDsQrd3D6NyWYu+gkTENkG5IKYhUIcxSb59SQC/hEQ+MrG4sTgBVghTonNWq42bA/d4yYumldQ==", + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.2.tgz", + "integrity": "sha512-f7rW7LJ2b7Uh2EiQ+7sza6RDZnajbNbemn54Ob6fRwQbgcIn+GWfyuHDHRYgRoZu1P4AayVScrRW+YfbTvPQoQ==", + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.46.2", + "@typescript-eslint/tsconfig-utils": "8.46.2", + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/visitor-keys": "8.46.2", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.2.tgz", + "integrity": "sha512-sExxzucx0Tud5tE0XqR0lT0psBQvEpnpiul9XbGUB1QwpWJJAps1O/Z7hJxLGiZLBKMCutjTzDgmd1muEhBnVg==", + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.46.2", + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/typescript-estree": "8.46.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.2.tgz", + "integrity": "sha512-tUFMXI4gxzzMXt4xpGJEsBsTox0XbNQ1y94EwlD/CuZwFcQP79xfQqMhau9HsRc/J0cAPA/HZt1dZPtGn9V/7w==", + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitejs/plugin-react-swc": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-4.2.0.tgz", + "integrity": "sha512-/tesahXD1qpkGC6FzMoFOJj0RyZdw9xLELOL+6jbElwmWfwOnIVy+IfpY+o9JfD9PKaR/Eyb6DNrvbXpuvA+8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-beta.43", + "@swc/core": "^1.13.5" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4 || ^5 || ^6 || ^7" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/birecord": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/birecord/-/birecord-0.1.1.tgz", + "integrity": "sha512-VUpsf/qykW0heRlC8LooCq28Kxn3mAqKohhDG/49rrsQ1dT1CXyj/pgXS+5BSRzFTR/3DyIBOqQOrGyZOh71Aw==", + "license": "(MIT OR Apache-2.0)" + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/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==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/compare-versions": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-6.1.1.tgz", + "integrity": "sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==", + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.11.tgz", + "integrity": "sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.11", + "@esbuild/android-arm": "0.25.11", + "@esbuild/android-arm64": "0.25.11", + "@esbuild/android-x64": "0.25.11", + "@esbuild/darwin-arm64": "0.25.11", + "@esbuild/darwin-x64": "0.25.11", + "@esbuild/freebsd-arm64": "0.25.11", + "@esbuild/freebsd-x64": "0.25.11", + "@esbuild/linux-arm": "0.25.11", + "@esbuild/linux-arm64": "0.25.11", + "@esbuild/linux-ia32": "0.25.11", + "@esbuild/linux-loong64": "0.25.11", + "@esbuild/linux-mips64el": "0.25.11", + "@esbuild/linux-ppc64": "0.25.11", + "@esbuild/linux-riscv64": "0.25.11", + "@esbuild/linux-s390x": "0.25.11", + "@esbuild/linux-x64": "0.25.11", + "@esbuild/netbsd-arm64": "0.25.11", + "@esbuild/netbsd-x64": "0.25.11", + "@esbuild/openbsd-arm64": "0.25.11", + "@esbuild/openbsd-x64": "0.25.11", + "@esbuild/openharmony-arm64": "0.25.11", + "@esbuild/sunos-x64": "0.25.11", + "@esbuild/win32-arm64": "0.25.11", + "@esbuild/win32-ia32": "0.25.11", + "@esbuild/win32-x64": "0.25.11" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.38.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.38.0.tgz", + "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.1", + "@eslint/core": "^0.16.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.38.0", + "@eslint/plugin-kit": "^0.4.0", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-dom": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-dom/-/eslint-plugin-react-dom-2.2.4.tgz", + "integrity": "sha512-mrr56eZsNF0m6NrZxV3wojQnxADLqYGB0A5FHYRuMEX8jmkOy0Jb7v6B4IdzLt0kI1HhAhriOogxOkFlCch/4w==", + "license": "MIT", + "dependencies": { + "@eslint-react/ast": "2.2.4", + "@eslint-react/core": "2.2.4", + "@eslint-react/eff": "2.2.4", + "@eslint-react/shared": "2.2.4", + "@eslint-react/var": "2.2.4", + "@typescript-eslint/scope-manager": "^8.46.2", + "@typescript-eslint/types": "^8.46.2", + "@typescript-eslint/utils": "^8.46.2", + "compare-versions": "^6.1.1", + "string-ts": "^2.2.1", + "ts-pattern": "^5.9.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "eslint": "^9.38.0", + "typescript": "^5.9.3" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.24.tgz", + "integrity": "sha512-nLHIW7TEq3aLrEYWpVaJ1dRgFR+wLDPN8e8FpYAql/bMV2oBEfC37K0gLEGgv9fy66juNShSMV8OkTqzltcG/w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-plugin-react-x": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-x/-/eslint-plugin-react-x-2.2.4.tgz", + "integrity": "sha512-mdoxE1SPt653/udAGQvwPob7ZgaPjIk47G0MWNwKtzLyuI0oD5X+6uq1QAn99TOM1q+sqjsCvvL7aMwGDuZ6aw==", + "license": "MIT", + "dependencies": { + "@eslint-react/ast": "2.2.4", + "@eslint-react/core": "2.2.4", + "@eslint-react/eff": "2.2.4", + "@eslint-react/shared": "2.2.4", + "@eslint-react/var": "2.2.4", + "@typescript-eslint/scope-manager": "^8.46.2", + "@typescript-eslint/type-utils": "^8.46.2", + "@typescript-eslint/types": "^8.46.2", + "@typescript-eslint/utils": "^8.46.2", + "compare-versions": "^6.1.1", + "is-immutable-type": "^5.0.1", + "string-ts": "^2.2.1", + "ts-api-utils": "^2.1.0", + "ts-pattern": "^5.9.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "eslint": "^9.38.0", + "typescript": "^5.9.3" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", + "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==", + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/framer-motion": { + "version": "12.23.25", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.25.tgz", + "integrity": "sha512-gUHGl2e4VG66jOcH0JHhuJQr6ZNwrET9g31ZG0xdXzT0CznP7fHX4P8Bcvuc4MiUB90ysNnWX2ukHRIggkl6hQ==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.23.23", + "motion-utils": "^12.23.6", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": 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==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", + "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/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==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-immutable-type": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/is-immutable-type/-/is-immutable-type-5.0.1.tgz", + "integrity": "sha512-LkHEOGVZZXxGl8vDs+10k3DvP++SEoYEAJLRk6buTFi6kD7QekThV7xHS0j6gpnUCQ0zpud/gMDGiV4dQneLTg==", + "license": "BSD-3-Clause", + "dependencies": { + "@typescript-eslint/type-utils": "^8.0.0", + "ts-api-utils": "^2.0.0", + "ts-declaration-location": "^1.0.4" + }, + "peerDependencies": { + "eslint": "*", + "typescript": ">=4.7.4" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", + "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.30.2", + "lightningcss-darwin-arm64": "1.30.2", + "lightningcss-darwin-x64": "1.30.2", + "lightningcss-freebsd-x64": "1.30.2", + "lightningcss-linux-arm-gnueabihf": "1.30.2", + "lightningcss-linux-arm64-gnu": "1.30.2", + "lightningcss-linux-arm64-musl": "1.30.2", + "lightningcss-linux-x64-gnu": "1.30.2", + "lightningcss-linux-x64-musl": "1.30.2", + "lightningcss-win32-arm64-msvc": "1.30.2", + "lightningcss-win32-x64-msvc": "1.30.2" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", + "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", + "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", + "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", + "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", + "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", + "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", + "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", + "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", + "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", + "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", + "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/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==", + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/motion": { + "version": "12.23.25", + "resolved": "https://registry.npmjs.org/motion/-/motion-12.23.25.tgz", + "integrity": "sha512-Fk5Y1kcgxYiTYOUjmwfXQAP7tP+iGqw/on1UID9WEL/6KpzxPr9jY2169OsjgZvXJdpraKXy0orkjaCVIl5fgQ==", + "license": "MIT", + "dependencies": { + "framer-motion": "^12.23.25", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/motion-dom": { + "version": "12.23.23", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.23.tgz", + "integrity": "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.23.6" + } + }, + "node_modules/motion-utils": { + "version": "12.23.6", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz", + "integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", + "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-cookie": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/react-cookie/-/react-cookie-8.0.1.tgz", + "integrity": "sha512-QNdAd0MLuAiDiLcDU/2s/eyKmmfMHtjPUKJ2dZ/5CcQ9QKUium4B3o61/haq6PQl/YWFqC5PO8GvxeHKhy3GFA==", + "license": "MIT", + "dependencies": { + "@types/hoist-non-react-statics": "^3.3.6", + "hoist-non-react-statics": "^3.3.2", + "universal-cookie": "^8.0.0" + }, + "peerDependencies": { + "react": ">= 16.3.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", + "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.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==", + "license": "MIT" + }, + "node_modules/react-router": { + "version": "7.9.5", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.5.tgz", + "integrity": "sha512-JmxqrnBZ6E9hWmf02jzNn9Jm3UqyeimyiwzD69NjxGySG6lIz/1LVPsoTCwN7NBX2XjCEa1LIX5EMz1j2b6u6A==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.9.5", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.9.5.tgz", + "integrity": "sha512-mkEmq/K8tKN63Ae2M7Xgz3c9l9YNbY+NHH6NNeUmLA3kDkhKXRsNb/ZpxaEunvGo2/3YXdk5EJU3Hxp3ocaBPw==", + "license": "MIT", + "dependencies": { + "react-router": "7.9.5" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz", + "integrity": "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.52.5", + "@rollup/rollup-android-arm64": "4.52.5", + "@rollup/rollup-darwin-arm64": "4.52.5", + "@rollup/rollup-darwin-x64": "4.52.5", + "@rollup/rollup-freebsd-arm64": "4.52.5", + "@rollup/rollup-freebsd-x64": "4.52.5", + "@rollup/rollup-linux-arm-gnueabihf": "4.52.5", + "@rollup/rollup-linux-arm-musleabihf": "4.52.5", + "@rollup/rollup-linux-arm64-gnu": "4.52.5", + "@rollup/rollup-linux-arm64-musl": "4.52.5", + "@rollup/rollup-linux-loong64-gnu": "4.52.5", + "@rollup/rollup-linux-ppc64-gnu": "4.52.5", + "@rollup/rollup-linux-riscv64-gnu": "4.52.5", + "@rollup/rollup-linux-riscv64-musl": "4.52.5", + "@rollup/rollup-linux-s390x-gnu": "4.52.5", + "@rollup/rollup-linux-x64-gnu": "4.52.5", + "@rollup/rollup-linux-x64-musl": "4.52.5", + "@rollup/rollup-openharmony-arm64": "4.52.5", + "@rollup/rollup-win32-arm64-msvc": "4.52.5", + "@rollup/rollup-win32-ia32-msvc": "4.52.5", + "@rollup/rollup-win32-x64-gnu": "4.52.5", + "@rollup/rollup-win32-x64-msvc": "4.52.5", + "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==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-ts": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/string-ts/-/string-ts-2.2.1.tgz", + "integrity": "sha512-Q2u0gko67PLLhbte5HmPfdOjNvUKbKQM+mCNQae6jE91DmoFHY6HH9GcdqCeNx87DZ2KKjiFxmA0R/42OneGWw==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tailwindcss": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.16.tgz", + "integrity": "sha512-pONL5awpaQX4LN5eiv7moSiSPd/DLDzKVRJz8Q9PgzmAdd1R4307GQS2ZpfiN7ZmekdQrfhZZiSE5jkLR4WNaA==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-declaration-location": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/ts-declaration-location/-/ts-declaration-location-1.0.7.tgz", + "integrity": "sha512-EDyGAwH1gO0Ausm9gV6T2nUvBgXT5kGoCMJPllOaooZ+4VvJiKBdZE7wK18N1deEowhcUptS+5GXZK8U/fvpwA==", + "funding": [ + { + "type": "ko-fi", + "url": "https://ko-fi.com/rebeccastevens" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/ts-declaration-location" + } + ], + "license": "BSD-3-Clause", + "dependencies": { + "picomatch": "^4.0.2" + }, + "peerDependencies": { + "typescript": ">=4.0.0" + } + }, + "node_modules/ts-declaration-location/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/ts-pattern": { + "version": "5.9.0", + "resolved": "https://registry.npmjs.org/ts-pattern/-/ts-pattern-5.9.0.tgz", + "integrity": "sha512-6s5V71mX8qBUmlgbrfL33xDUwO0fq48rxAu2LBE11WBeGdpCPOsXksQbZJHvHwhrd3QjUusd3mAOM5Gg0mFBLg==", + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.46.2.tgz", + "integrity": "sha512-vbw8bOmiuYNdzzV3lsiWv6sRwjyuKJMQqWulBOU7M0RrxedXledX8G8kBbQeiOYDnTfiXz0Y4081E1QMNB6iQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.46.2", + "@typescript-eslint/parser": "8.46.2", + "@typescript-eslint/typescript-estree": "8.46.2", + "@typescript-eslint/utils": "8.46.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/universal-cookie": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/universal-cookie/-/universal-cookie-8.0.1.tgz", + "integrity": "sha512-B6ks9FLLnP1UbPPcveOidfvB9pHjP+wekP2uRYB9YDfKVpvcjKgy1W5Zj+cEXJ9KTPnqOKGfVDQBmn8/YCQfRg==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.2" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "7.1.12", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz", + "integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==", + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz", + "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000..8ff4695 --- /dev/null +++ b/web/package.json @@ -0,0 +1,39 @@ +{ + "name": "web", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@tailwindcss/vite": "^4.1.16", + "axios": "^1.13.2", + "eslint-plugin-react-dom": "^2.2.4", + "eslint-plugin-react-x": "^2.2.4", + "motion": "^12.23.25", + "react": "^19.1.1", + "react-cookie": "^8.0.1", + "react-dom": "^19.1.1", + "react-router-dom": "^7.9.5", + "tailwindcss": "^4.1.16" + }, + "devDependencies": { + "@eslint/js": "^9.36.0", + "@types/node": "^24.6.0", + "@types/react": "^19.1.16", + "@types/react-dom": "^19.1.9", + "@vitejs/plugin-react-swc": "^4.1.0", + "eslint": "^9.36.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.22", + "globals": "^16.4.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.45.0", + "vite": "^7.1.7" + } +} diff --git a/web/public/vite.svg b/web/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/web/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/src/App.tsx b/web/src/App.tsx new file mode 100644 index 0000000..a4ef3b0 --- /dev/null +++ b/web/src/App.tsx @@ -0,0 +1,62 @@ +import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom'; +import Home from './pages/Home'; +import WebLayout from "./layouts/WebLayout"; +import NotFound from './pages/NotFound'; + +import ROUTE_CONSTANTS from './types/routes'; +import Create from './pages/Create'; +import Favorites from './pages/Favorites'; +import Profile from './pages/Profile'; +import ShoppingList from './pages/ShoppingList'; +import LoginPage from './pages/Login'; +import { use, type ReactNode } from 'react'; +import { AuthContext } from './context/AuthContext'; +import RecipePage from './pages/Recipe'; +import SearchPage from './pages/Search'; + +function ProtectedRoute({ children }: { children: ReactNode }) { + const { isLoggedIn } = use(AuthContext) + // Wait until the value is set + if (isLoggedIn === undefined) { + // Still checking auth state: don't render anything yet, or show a spinner if desired + return null; // or + } + + if (isLoggedIn) return children; + + // Redirect to login page if not authenicated + return +} + +function App() { + return ( + + + } /> + + {/* Login page does not inherit WebLayout */} + } /> + + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + } /> + + + + {/* 404: Not Found */} + }> + } /> + + + + ); +} + +export default App diff --git a/web/static/img/recipe_placeholder.png b/web/src/assets/images/recipe_placeholder.png similarity index 100% rename from web/static/img/recipe_placeholder.png rename to web/src/assets/images/recipe_placeholder.png diff --git a/web/static/img/recipe_placeholder_wide.jpg b/web/src/assets/images/recipe_placeholder_wide.jpg similarity index 100% rename from web/static/img/recipe_placeholder_wide.jpg rename to web/src/assets/images/recipe_placeholder_wide.jpg diff --git a/web/src/assets/react.svg b/web/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/web/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/static/img/salmon_video.mp4 b/web/src/assets/videos/salmon_video.mp4 similarity index 100% rename from web/static/img/salmon_video.mp4 rename to web/src/assets/videos/salmon_video.mp4 diff --git a/web/src/components/Banner.tsx b/web/src/components/Banner.tsx new file mode 100644 index 0000000..f7f2fe6 --- /dev/null +++ b/web/src/components/Banner.tsx @@ -0,0 +1,12 @@ + +interface BannerProps { + content: string; +}; + +export default function Banner({ content }: BannerProps) { + return ( +

    + {content} +

    + ); +} diff --git a/web/src/components/Navigation.tsx b/web/src/components/Navigation.tsx new file mode 100644 index 0000000..9efb742 --- /dev/null +++ b/web/src/components/Navigation.tsx @@ -0,0 +1,116 @@ +import { useState } from "react"; + +import ROUTE_CONSTANTS from "../types/routes.ts"; +import { useLocation } from "react-router-dom"; +import ShoppingListIcon from "./icons/ShoppingListIcon.tsx"; + +export default function Navigation() { + const [displayHamburgerMenu, setDisplayHamburgerMenu] = useState(false); + + const location = useLocation(); + + return ( + <> + + + + + ); + +} + +interface HamburgerMenuProps { + show: boolean; +}; + +function HamburgerMenu({ show }: HamburgerMenuProps) { + return ( +
    + + + + + +
    + ); +} + +interface DropdownLinkProps { + name: string; + url: string; +} + +function DropdownLink({ name, url }: DropdownLinkProps) { + return ( + + {name} + + ); +}; + +interface NavigationLinkProps { + name: string; + url: string; + current: boolean; +} + +function NavigationLink({ name, url, current }: NavigationLinkProps) { + return ( + + {name} + + ); +} + +interface IconNavigationLinkProps { + icon: React.ReactElement; + url: string; +} + +function IconNavigationLink({ icon, url }: IconNavigationLinkProps) { + return ( + + {icon} + + ); +} diff --git a/web/src/components/Spinner.tsx b/web/src/components/Spinner.tsx new file mode 100644 index 0000000..e8c536e --- /dev/null +++ b/web/src/components/Spinner.tsx @@ -0,0 +1,12 @@ +interface SpinnerProps { + content: string; +} + +export default function Spinner({ content }: SpinnerProps) { + return ( + <> +
    +

    {content}

    + + ); +} diff --git a/web/src/components/buttons/DropdownButton.tsx b/web/src/components/buttons/DropdownButton.tsx new file mode 100644 index 0000000..f1de8b3 --- /dev/null +++ b/web/src/components/buttons/DropdownButton.tsx @@ -0,0 +1,19 @@ + +interface DropdownButtonProps { + content: string; + name: string; + value: string; + selected: boolean; + changeHandler: (e: React.ChangeEvent) => void; +} + +export default function DropdownButton({ content, name, value, selected, changeHandler }: DropdownButtonProps) { + return ( + + ); +} diff --git a/web/src/components/buttons/FavoriteButton.tsx b/web/src/components/buttons/FavoriteButton.tsx new file mode 100644 index 0000000..d7a71be --- /dev/null +++ b/web/src/components/buttons/FavoriteButton.tsx @@ -0,0 +1,78 @@ +import { use, useEffect, useState } from "react"; +import { AuthContext } from "../../context/AuthContext"; +import { useNavigate } from "react-router-dom"; +import { EngagementFavoriteRecipe } from "../../services/EngagementService"; +import { isApiError } from "../../types/api/error"; + + +interface FavoriteButtonProps { + favorite: boolean | undefined; + id: number | undefined; +} + +export default function FavoriteButton({ favorite, id }: FavoriteButtonProps) { + // CONTEXT + const { isLoggedIn } = use(AuthContext); + const navigate = useNavigate(); + + const [_favorite, setFavorite] = useState(); + + const clickHandler = async () => { + // This button cannot be used if not logged in + if (!isLoggedIn) { + await navigate("/v2/web/login"); + return; + } + + if (!id) return; + + // Toggle button first, to feel fast + setFavorite(!_favorite); + + const result = await EngagementFavoriteRecipe(id); + if (isApiError(result)) { + console.error(result.message); + } + } + + useEffect(() => { + if (favorite) + setFavorite(favorite); + + }, [favorite]); + + return _favorite ? ( + + + ) : ( + + + ); +} diff --git a/web/src/components/buttons/FilterButton.tsx b/web/src/components/buttons/FilterButton.tsx new file mode 100644 index 0000000..86bdfca --- /dev/null +++ b/web/src/components/buttons/FilterButton.tsx @@ -0,0 +1,36 @@ + +interface FilterButtonProps { + click: () => void; +} + +export default function FilterButton({ click }: FilterButtonProps) { + + return ( + + ); +} diff --git a/web/src/components/buttons/LikeButton.tsx b/web/src/components/buttons/LikeButton.tsx new file mode 100644 index 0000000..d2ffe83 --- /dev/null +++ b/web/src/components/buttons/LikeButton.tsx @@ -0,0 +1,10 @@ +export default function LikeButton() { + return ( + + + + ); +} diff --git a/web/src/components/buttons/MadeButton.tsx b/web/src/components/buttons/MadeButton.tsx new file mode 100644 index 0000000..d0d5165 --- /dev/null +++ b/web/src/components/buttons/MadeButton.tsx @@ -0,0 +1,61 @@ +import { use, useState } from "react"; +import { AuthContext } from "../../context/AuthContext"; +import { useNavigate } from "react-router-dom"; +import { EngagementMakeRecipe } from "../../services/EngagementService"; +import { isApiError } from "../../types/api/error"; + +interface MadeButtonProps { + id: number; +} + +export default function MadeButton({ id }: MadeButtonProps) { + // CONTEXT + const { isLoggedIn } = use(AuthContext); + const navigate = useNavigate(); + + const [clicked, setClicked] = useState(false); + + const clickHandler = async () => { + // This button cannot be used if not logged in + if (!isLoggedIn) { + await navigate("/v2/web/login"); + return; + } + + if (!id || clicked) return; + + // Toggle button first, to feel fast + setClicked(true); + + const result = await EngagementMakeRecipe(id); + if (isApiError(result)) { + console.error(result.message); + } + } + + return ( + + + ); +} diff --git a/web/src/components/buttons/ShareButton.tsx b/web/src/components/buttons/ShareButton.tsx new file mode 100644 index 0000000..08e886e --- /dev/null +++ b/web/src/components/buttons/ShareButton.tsx @@ -0,0 +1,56 @@ +import { useState } from "react"; +import { EngagementShareRecipe } from "../../services/EngagementService"; +import { isApiError } from "../../types/api/error"; + + +interface ShareButtonProps { + id: number; +} + +// TODO: Abstract this somehow, this needs to be loaded from the env +const DOMAIN = "http://localhost:5173/"; + +export default function ShareButton({ id }: ShareButtonProps) { + const [clicked, setClicked] = useState(false); + + const clickHandler = async () => { + if (clicked) return; + + // Copy first, so it feels fast + await navigator.clipboard.writeText(`${DOMAIN}/v2/web/recipe/${id}`) + + const result = await EngagementShareRecipe(id); + if (isApiError(result)) { + console.error(result.message); + } + + setClicked(true); + }; + + return clicked ? ( + + ) : ( + + ); +} diff --git a/web/src/components/cards/ContentCardSmall.tsx b/web/src/components/cards/ContentCardSmall.tsx new file mode 100644 index 0000000..882b7ad --- /dev/null +++ b/web/src/components/cards/ContentCardSmall.tsx @@ -0,0 +1,17 @@ + +interface ContentCardSmallProps { + content: string; + target: string; +}; + +export default function ContentCardSmall({ content, target }: ContentCardSmallProps) { + return ( + + ); +} diff --git a/web/src/components/cards/RecipeCardLarge.tsx b/web/src/components/cards/RecipeCardLarge.tsx new file mode 100644 index 0000000..00da616 --- /dev/null +++ b/web/src/components/cards/RecipeCardLarge.tsx @@ -0,0 +1,60 @@ +import type { Recipe } from "../../types/recipe"; +import RecipePlaceholder from "../../assets/images/recipe_placeholder.png" +import LikeButton from "../buttons/LikeButton"; +import { useNavigate } from "react-router-dom"; +import { EngagementViewRecipe } from "../../services/EngagementService"; +import { isApiError } from "../../types/api/error"; + +interface RecipeCardLargeProps { + recipe: Recipe | null; +} + +export default function RecipeCardLarge({ recipe }: RecipeCardLargeProps) { + const navigate = useNavigate(); + + // HANDLERS + const makeButtonHandler = async () => { + if (!recipe) return; + + // Navigate first, so it feels faster + await navigate(`/v2/web/recipe/${recipe.Id}`); + + const result = await EngagementViewRecipe(recipe.Id); + if (isApiError(result)) { + console.error(result.message); + } + } + + if (recipe == null) { + return

    Coming soon!

    + } + + return ( +
    + +
    +

    + {recipe.Title} +

    +

    + Serves {recipe.Serves} +

    +

    + {recipe.Description} +

    +
    +

    + {recipe.Category} - {recipe.Duration.Total} mins +

    + {recipe.Favorite && } +
    + +
    +
    + ); +} diff --git a/web/src/components/cards/RecipeCardSmall.tsx b/web/src/components/cards/RecipeCardSmall.tsx new file mode 100644 index 0000000..e8a86b0 --- /dev/null +++ b/web/src/components/cards/RecipeCardSmall.tsx @@ -0,0 +1,51 @@ +import type { Recipe } from "../../types/recipe"; +import RecipePlaceholder from "../../assets/images/recipe_placeholder.png" +import LikeButton from "../buttons/LikeButton"; +import { EngagementViewRecipe } from "../../services/EngagementService"; +import { isApiError } from "../../types/api/error"; +import { useNavigate } from "react-router-dom"; + +interface RecipeCardSmallProps { + recipe: Recipe; +} + +export default function RecipeCardSmall({ recipe }: RecipeCardSmallProps) { + const navigate = useNavigate(); + + // HANDLERS + const makeButtonHandler = async () => { + // Navigate first, so it feels faster + await navigate(`/v2/web/recipe/${recipe.Id}`); + + const result = await EngagementViewRecipe(recipe.Id); + if (isApiError(result)) { + console.error(result.message); + } + } + + return ( +
    + +
    +

    + {recipe.Title} +

    +

    + Serves {recipe.Serves} +

    +
    +

    + {recipe.Category} - {recipe.Duration.Total} mins +

    + {recipe.Favorite && } +
    + +
    +
    + ); +} diff --git a/web/src/components/display/RecipeMetaData.tsx b/web/src/components/display/RecipeMetaData.tsx new file mode 100644 index 0000000..bbaccad --- /dev/null +++ b/web/src/components/display/RecipeMetaData.tsx @@ -0,0 +1,58 @@ +import type { Recipe } from "../../types/recipe" +import ServingSizeIcon from "../icons/ServingSizeIcon" +import StarIcon from "../icons/StarIcon" +import TimeIcon from "../icons/TimeIcon" + +function displayDifficulty(diff: number): string { + switch (diff) { + case 1: + return "Beginner" + case 2: + return "Easy" + case 3: + return "Intermediate" + case 4: + return "Challenging" + case 5: + return "Extreme" + default: + return "" + } +} + +interface RecipeMetaDataProps { + recipe: Recipe | null; +} + +export default function RecipeMetaData({ recipe }: RecipeMetaDataProps) { + return ( +
    +
    + +

    Prep: {recipe?.Duration.Prep ?? 0} min

    +

    Cook: {recipe?.Duration.Cook ?? 0} min

    +
    +
    +
    + {Array.from({ length: recipe?.Difficulty ?? 0 }).map((_, i) => ( + + ))} + {Array.from({ length: 5 - (recipe?.Difficulty ?? 0) }).map((_, i) => ( + + ))} +
    +

    {displayDifficulty(recipe?.Difficulty ?? 0)}

    +
    +
    + +

    Serves {recipe?.Serves ?? 0}

    +
    +
    + + ); +} diff --git a/web/src/components/forms/IngredientItem.tsx b/web/src/components/forms/IngredientItem.tsx new file mode 100644 index 0000000..a6174b7 --- /dev/null +++ b/web/src/components/forms/IngredientItem.tsx @@ -0,0 +1,94 @@ +import { Reorder, useDragControls } from "motion/react"; +import { INGREDIENT_UNITS, type RecipeIngredient } from "../../types/recipe"; +import DeleteIconSmall from "../icons/DeleteIconSmall"; +import DragIconSmall from "../icons/DragIconSmall"; + +interface IngredientItemProps { + classes: string; + ingredient: RecipeIngredient; + onChange: (id: string, name: "Amount" | "Unit" | "Name", value: string) => void; + removeIngredientHandler: (id: string) => void; + allowDelete: boolean; + valid: boolean; + dirty: boolean; + markDirty: (id: string) => void; +} + +export default function IngredientItem({ classes, ingredient, onChange, removeIngredientHandler, allowDelete, valid, dirty, markDirty }: IngredientItemProps) { + const controls = useDragControls(); + + const changeHandler = (name: "Amount" | "Unit" | "Name", value: string) => { + if (!dirty) markDirty(ingredient.Id); + onChange(ingredient.Id, name, value); + } + + return ( + +
    +
    +
    + changeHandler("Amount", e.target.value)} + className={`w-1/2 md:w-28 ${classes} ${dirty && ingredient.Amount <= 0 ? "border-red-500" : ""}`} + /> + + +
    + + changeHandler("Name", e.target.value)} + className={`flex-grow ${classes} ${dirty && ingredient.Name.trim() === "" ? "border-red-500" : ""}`} + /> +
    + +
    + +
    { + e.preventDefault(); + controls.start(e); + }} + className="p-1 md:p-0 cursor-pointer touch-none" + > + +
    +
    +
    + + {(dirty && !valid) && ( +

    Please fill out all fields.

    + )} +
    + ); +} diff --git a/web/src/components/forms/IngredientList.tsx b/web/src/components/forms/IngredientList.tsx new file mode 100644 index 0000000..8d36b29 --- /dev/null +++ b/web/src/components/forms/IngredientList.tsx @@ -0,0 +1,47 @@ +import { Reorder } from "motion/react"; +import type { RecipeIngredient, RecipeIngredientSection } from "../../types/recipe"; +import IngredientItem from "./IngredientItem"; +import type { RecipeValidationEntry } from "../../pages/Create"; + +interface IngredientListProps { + classes: string; + section: RecipeIngredientSection; + ingredients: RecipeIngredient[]; + setSectionIngredients: (sectionId: string, ingredients: RecipeIngredient[]) => void; + ingredientChangeHandler: (id: string, name: "Amount" | "Unit" | "Name", value: string) => void; + removeIngredientHandler: (id: string) => void; + validList: RecipeValidationEntry[]; + dirtyList: Record; + markDirty: (id: string) => void; +} + +export default function IngredientList({ classes, section, ingredients, setSectionIngredients, ingredientChangeHandler, removeIngredientHandler, validList, dirtyList, markDirty }: IngredientListProps) { + const sectionIngredients = ingredients.filter(x => x.SectionId === section.Id); + + const reorderHandler = (ingredients: RecipeIngredient[]) => { + setSectionIngredients(section.Id, ingredients); + } + + return ( + + {sectionIngredients.map(ingredient => + 1} + valid={validList.find(x => x.id === ingredient.Id)?.valid ?? true} + dirty={dirtyList[ingredient.Id] ?? false} + markDirty={markDirty} + /> + )} + + ); +} diff --git a/web/src/components/forms/IngredientSection.tsx b/web/src/components/forms/IngredientSection.tsx new file mode 100644 index 0000000..1748a1a --- /dev/null +++ b/web/src/components/forms/IngredientSection.tsx @@ -0,0 +1,59 @@ +import { Reorder, useDragControls } from "motion/react"; +import type { RecipeIngredientSection } from "../../types/recipe"; +import DeleteIconSmall from "../icons/DeleteIconSmall"; +import DragIconSmall from "../icons/DragIconSmall"; +import type { ReactNode } from "react"; + +interface IngredientSectionProps { + section: RecipeIngredientSection; + onChange: (id: string, name: string) => void; + removeIngredientSectionHandler: (id: string) => void; + allowDelete: boolean; + children?: ReactNode; +}; + +export default function IngredientSection({ section, onChange, removeIngredientSectionHandler, allowDelete, children }: IngredientSectionProps) { + const controls = useDragControls(); + + return ( + +
    +

    Group:

    + onChange(section.Id, e.target.value)} + placeholder="Group label" + className="mx-2 px-2 py-1 border border-gray-300 flex-grow rounded-sm min-w-1" + /> + +
    + +
    { + e.preventDefault(); + controls.start(e); + }} + > + +
    +
    +
    + {children} +
    + ); +} + diff --git a/web/src/components/forms/InstructionElement.tsx b/web/src/components/forms/InstructionElement.tsx new file mode 100644 index 0000000..79c8c0b --- /dev/null +++ b/web/src/components/forms/InstructionElement.tsx @@ -0,0 +1,76 @@ +import { type ChangeEvent } from "react"; +import type { RecipeInstruction } from "../../types/recipe"; +import { Reorder, useDragControls } from "motion/react"; +import DragIconSmall from "../icons/DragIconSmall"; +import DeleteIconSmall from "../icons/DeleteIconSmall"; + +interface InstructionElementProps { + instruction: RecipeInstruction; + index: number; + allowDelete: boolean; + onChange: (id: string, value: string) => void; + onDelete: (id: string) => void; + valid: boolean; + dirty: boolean; + markDirty: (id: string) => void; +} + +export default function InstructionElement({ instruction, index, allowDelete, onChange, onDelete, valid, dirty, markDirty }: InstructionElementProps) { + const controls = useDragControls(); + + const changeHandler = (e: ChangeEvent) => { + if (!dirty) markDirty(instruction.Id) + onChange(instruction.Id, e.target.value); + } + + return ( + +
    +

    {index + 1}.

    +
    +