commit e7987e88a7b1e6f8cf8bad27ad2525e3c167f900 Author: Daniel Noulet Date: Wed Oct 7 15:21:47 2026 +0200 initial commit diff --git a/.air.toml b/.air.toml new file mode 100644 index 0000000..c3e4d43 --- /dev/null +++ b/.air.toml @@ -0,0 +1,41 @@ +# Air configuration for hot reload +# https://github.com/air-verse/air + +root: "." +tmp_dir: "tmp" + +build: + args_bin: [] + bin: "./tmp/main" + cmd: "go build -o ./tmp/main ./cmd/server" + delay: 1000 + exclude_dir: ["assets", "tmp", "vendor", "web", "build", "docs", "deploy"] + exclude_file: ["config.yaml"] + exclude_regex: ["_test\\.go", "\\.yaml$"] + exclude_unchanged: true + follow_symlink: true + full_bin: "" + include_dir: [] + include_ext: ["go", "tpl", "tmpl", "html"] + kill_delay: "0s" + log: "build-errors.log" + poll: true + poll_interval: 300 + rerun: false + rerun_delay: 500 + send_interrupt: false + stop_on_error: true + +color: + app: "" + build: "yellow" + main: "magenta" + runner: "green" + watcher: "cyan" + +log: + main_only: false + time: false + +misc: + clean_on_exit: true \ No newline at end of file diff --git a/.env.local.example b/.env.local.example new file mode 100644 index 0000000..422a151 --- /dev/null +++ b/.env.local.example @@ -0,0 +1,46 @@ +# Local development environment variables for go-first +# Copy to .env.local and fill in values + +# Server +SERVER_MODE=debug +SERVER_HOST=0.0.0.0 +SERVER_PORT=8080 + +# Database (use local PostgreSQL or Docker) +DATABASE_HOST=localhost +DATABASE_PORT=5432 +DATABASE_USER=postgres +DATABASE_PASSWORD=postgres +DATABASE_NAME=go_first_dev +DATABASE_SSL_MODE=disable + +# Redis (use local Redis or Docker) +REDIS_HOST=localhost +REDIS_PORT=6379 +REDIS_PASSWORD= +REDIS_DB=0 + +# JWT +JWT_SECRET=local_development_secret_key_change_in_production +JWT_EXPIRES_IN=24 +JWT_REFRESH_EXPIRES_IN=168 + +# CORS +CORS_ALLOWED_ORIGINS=http://localhost:3000,http://localhost:5173,http://127.0.0.1:3000,http://127.0.0.1:5173 + +# Logging +LOGGING_LEVEL=debug +LOGGING_FORMAT=console +LOGGING_OUTPUT=stdout + +# Email (using mock service in development) +EMAIL_SMTP_HOST= +EMAIL_SMTP_PORT=587 +EMAIL_USERNAME= +EMAIL_PASSWORD= +EMAIL_FROM_ADDRESS=noreply@localhost +EMAIL_FROM_NAME=URPI Local + +# Frontend +FRONTEND_BUILD_PATH=./web/dist +FRONTEND_SPA_FALLBACK=true \ No newline at end of file diff --git a/.env.production.example b/.env.production.example new file mode 100644 index 0000000..12d4e44 --- /dev/null +++ b/.env.production.example @@ -0,0 +1,46 @@ +# Production environment variables for go-first +# Copy to .env.production and fill in values + +# Server +SERVER_MODE=release +SERVER_HOST=0.0.0.0 +SERVER_PORT=8080 + +# Database +DATABASE_HOST=postgres +DATABASE_PORT=5432 +DATABASE_USER=postgres +DATABASE_PASSWORD=CHANGE_ME_SECURE_PASSWORD +DATABASE_NAME=go_first +DATABASE_SSL_MODE=disable + +# Redis +REDIS_HOST=redis +REDIS_PORT=6379 +REDIS_PASSWORD= +REDIS_DB=0 + +# JWT +JWT_SECRET=CHANGE_ME_VERY_LONG_RANDOM_SECRET_KEY_AT_LEAST_32_CHARS +JWT_EXPIRES_IN=24 +JWT_REFRESH_EXPIRES_IN=168 + +# CORS +CORS_ALLOWED_ORIGINS=https://www.urpi.be,https://urpi.be + +# Logging +LOGGING_LEVEL=info +LOGGING_FORMAT=json +LOGGING_OUTPUT=stdout + +# Email (SMTP) +EMAIL_SMTP_HOST=smtp.gmail.com +EMAIL_SMTP_PORT=587 +EMAIL_USERNAME=your-email@gmail.com +EMAIL_PASSWORD=your-app-password +EMAIL_FROM_ADDRESS=noreply@urpi.be +EMAIL_FROM_NAME=URPI + +# Frontend +FRONTEND_BUILD_PATH=./web/dist +FRONTEND_SPA_FALLBACK=true \ No newline at end of file diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml new file mode 100644 index 0000000..8977d03 --- /dev/null +++ b/.github/workflows/ci-cd.yml @@ -0,0 +1,297 @@ +name: CI/CD + +on: + push: + branches: [main, develop] + pull_request: + branches: [main] + release: + types: [published] + +env: + GO_VERSION: '1.23' + NODE_VERSION: '20' + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + # =========================================== + # Lint & Format + # =========================================== + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + cache: true + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + cache-dependency-path: web/package-lock.json + + - name: Install golangci-lint + uses: golangci/golangci-lint-action@v6 + with: + version: latest + + - name: Run Go linters + run: | + cd ${{ github.workspace }} + golangci-lint run ./... + + - name: Install frontend dependencies + run: | + cd web + npm ci + + - name: Run frontend linters + run: | + cd web + npm run lint + + - name: Check formatting + run: | + cd web + npm run format -- --check + + # =========================================== + # Test + # =========================================== + test: + name: Test + runs-on: ubuntu-latest + services: + postgres: + image: postgres:16-alpine + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: go_first_test + ports: 5432:5432 + options: >- + --health-cmd "pg_isready -U postgres" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + redis: + image: redis:7-alpine + ports: 6379:6379 + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + cache: true + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + cache-dependency-path: web/package-lock.json + + - name: Install Go dependencies + run: go mod download + + - name: Run Go tests + run: | + go test -v -race -coverprofile=coverage.out ./... + go tool cover -func=coverage.out + + - name: Upload Go coverage + uses: codecov/codecov-action@v4 + with: + files: coverage.out + flags: backend + + - name: Install frontend dependencies + run: | + cd web + npm ci + + - name: Run frontend tests + run: | + cd web + npm test -- --run --coverage + + - name: Upload frontend coverage + uses: codecov/codecov-action@v4 + with: + files: web/coverage/lcov.info + flags: frontend + + # =========================================== + # Build + # =========================================== + build: + name: Build + runs-on: ubuntu-latest + needs: [lint, test] + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + cache: true + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + cache-dependency-path: web/package-lock.json + + - name: Build backend + run: | + CGO_ENABLED=0 go build -ldflags="-s -w" -o build/go-first ./cmd/server + + - name: Build frontend + run: | + cd web + npm ci + npm run build + + - name: Upload backend artifact + uses: actions/upload-artifact@v4 + with: + name: go-first-backend + path: build/go-first + retention-days: 7 + + - name: Upload frontend artifact + uses: actions/upload-artifact@v4 + with: + name: go-first-frontend + path: web/dist + retention-days: 7 + + # =========================================== + # Docker Build & Push + # =========================================== + docker: + name: Docker + runs-on: ubuntu-latest + needs: build + if: github.event_name == 'push' || github.event_name == 'release' + permissions: + contents: read + packages: write + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=sha + + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + # =========================================== + # Deploy Staging + # =========================================== + deploy-staging: + name: Deploy Staging + runs-on: ubuntu-latest + needs: docker + if: github.ref == 'refs/heads/develop' + environment: staging + steps: + - name: Deploy to staging + run: | + echo "Deploying to staging..." + # Add your staging deployment commands here + # Example: kubectl apply -k deploy/kubernetes/overlays/staging + # Or: ssh deploy@staging-server "cd /opt/go-first && ./deploy/deploy.sh staging" + + # =========================================== + # Deploy Production + # =========================================== + deploy-production: + name: Deploy Production + runs-on: ubuntu-latest + needs: docker + if: github.event_name == 'release' + environment: production + steps: + - name: Deploy to production + run: | + echo "Deploying to production..." + # Add your production deployment commands here + # Example: kubectl apply -k deploy/kubernetes/overlays/production + # Or: ssh deploy@production-server "cd /opt/go-first && ./deploy/deploy.sh production" + + # =========================================== + # Security Scan + # =========================================== + security: + name: Security Scan + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Run Gosec + uses: securego/gosec@master + with: + args: './...' + + - name: Run Trivy + uses: aquasecurity/trivy-action@master + with: + scan-type: 'fs' + scan-ref: '.' + format: 'sarif' + output: 'trivy-results.sarif' + + - name: Upload Trivy results + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: 'trivy-results.sarif' \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1ca9dc1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,75 @@ +# Binaries +/go-first +/main +/tmp/ +/build/ + +# Go +go.work +*.exe +*.test +*.prof +coverage.out +coverage.html +**/dist +# Node +web/node_modules/ +web/dist/ +web/.vite/ +web/.turbo/ +web/*.log +web/npm-debug.log* +web/yarn-debug.log* +web/yarn-error.log* +web/pnpm-debug.log* + +# Environment +.env +.env.local +.env.production +.env.staging +.env.*.local + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ +.DS_Store + +# Docker +docker-compose.override.yml + +# Logs +*.log +logs/ + +# Database +*.db +*.sqlite +migrations/*.sql~ + +# OS +Thumbs.db +ehthumbs.db +Desktop.ini + +# Test +*.out +*.coverprofile + +# Certificates +deploy/ssl/ +*.pem +*.key +*.crt + +# Temporary +tmp/ +temp/ +*.tmp + +# Misc +*.bak +*.backup \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..a20ce3d --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,80 @@ +# Pre-commit hooks configuration +# Install: pip install pre-commit && pre-commit install +# Run: pre-commit run --all-files + +repos: + # Go formatting + - repo: local + hooks: + - id: go-fmt + name: go fmt + entry: go fmt ./... + language: system + types: [go] + pass_filenames: false + + - id: go-imports + name: go imports + entry: goimports -w . + language: system + types: [go] + pass_filenames: false + + - id: go-mod-tidy + name: go mod tidy + entry: go mod tidy + language: system + types: [go] + pass_filenames: false + + # Go linting + - repo: https://github.com/golangci/golangci-lint + rev: v1.56.0 + hooks: + - id: golangci-lint + args: [--fast] + + # General file checks + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-json + - id: check-toml + - id: check-merge-conflict + - id: detect-private-key + - id: detect-aws-credentials + - id: mixed-line-ending + args: [--fix=lf] + + # Prettier for frontend + - repo: https://github.com/pre-commit/mirrors-prettier + rev: v3.1.0 + hooks: + - id: prettier + types: [typescript, json, yaml, markdown, css, html] + exclude: ^web/node_modules/ + + # Secret detection + - repo: https://github.com/truffleHQ/truffleHog + rev: v3.70.0 + hooks: + - id: truffleHog + args: [--no-verify] + + # Spell check + - repo: https://github.com/crateci/typos + rev: v1.18.0 + hooks: + - id: typos + args: [--config, .typos.toml] + + # Markdown linting + - repo: https://github.com/igorshubovych/markdownlint-cli + rev: v0.39.0 + hooks: + - id: markdownlint + args: [--fix] + exclude: ^web/node_modules/ \ No newline at end of file diff --git a/.typos.toml b/.typos.toml new file mode 100644 index 0000000..7b85069 --- /dev/null +++ b/.typos.toml @@ -0,0 +1,32 @@ +# Typos configuration +# https://github.com/crateci/typos + +[default] +extend-ignore-regex = [ + "node_modules", + "dist", + "build", + "tmp", + ".git", + "vendor", +] + +[files] +extend-exclude = [ + "*.svg", + "*.png", + "*.jpg", + "*.jpeg", + "*.gif", + "*.ico", + "*.woff", + "*.woff2", + "*.ttf", + "*.eot", +] + +[default.extend-identifiers] +# Project-specific terms +urpi = "urpi" +gobananas = "go-bananas" +gofirst = "go-first" \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3533665 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,68 @@ +# Build stage for Go backend +FROM golang:1.23-alpine AS backend-builder + +WORKDIR /app + +# Install build dependencies +RUN apk add --no-cache git make + +# Copy go mod files +COPY go.mod go.sum ./ +RUN go mod download + +# Copy source code +COPY . . + +# Build backend +RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /go-first ./cmd/server + +# Build stage for React frontend +FROM node:20-alpine AS frontend-builder + +WORKDIR /web + +# Copy package files +COPY web/package*.json ./ +RUN npm ci + +# Copy source and build +COPY web/ ./ +RUN npm run build + +# Final stage +FROM alpine:3.20 + +# Install runtime dependencies +RUN apk add --no-cache ca-certificates tzdata + +# Create non-root user +RUN addgroup -g 1000 -S appgroup && \ + adduser -u 1000 -S appuser -G appgroup + +WORKDIR /app + +# Copy backend binary +COPY --from=backend-builder /go-first . + +# Copy frontend build +COPY --from=frontend-builder /web/dist ./web/dist + +# Copy config and the project catalog used as the initial import. +COPY config.yaml . +COPY projects.json . + +# Create directories for uploads, logs, etc. +RUN mkdir -p /app/uploads /app/logs && \ + chown -R appuser:appgroup /app + +USER appuser + +# Expose port +EXPOSE 8080 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:8080/api/v1/health || exit 1 + +# Run +ENTRYPOINT ["./go-first"] \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..31a0a57 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 URPI + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..db9ad4a --- /dev/null +++ b/Makefile @@ -0,0 +1,252 @@ +# go-first Makefile +# Web application for URPI - https://www.urpi.be + +.PHONY: help build run dev test lint fmt vet tidy docker-build docker-run docker-push deploy clean + +# Variables +APP_NAME := go-first +VERSION := $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev") +BUILD_TIME := $(shell date -u +%Y-%m-%dT%H:%M:%SZ) +GIT_COMMIT := $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown") +GO_VERSION := 1.23 +NODE_VERSION := 20 + +# Go variables +GO := go +GOFLAGS := -ldflags="-s -w -X main.version=$(VERSION) -X main.buildTime=$(BUILD_TIME) -X main.gitCommit=$(GIT_COMMIT)" +CGO_ENABLED := 0 + +# Docker variables +DOCKER_REGISTRY := ghcr.io +DOCKER_IMAGE := $(DOCKER_REGISTRY)/dnoulet/$(APP_NAME) +DOCKER_TAG := $(VERSION) + +# Directories +BUILD_DIR := ./build +WEB_DIR := ./web +DIST_DIR := $(WEB_DIR)/dist +SERVER_DIR := ./cmd/server + +# Default target +help: ## Show this help message + @echo "$(APP_NAME) - URPI Web Application" + @echo "" + @echo "Usage: make [target]" + @echo "" + @echo "Development:" + @echo " dev Run development servers (Go + Vite)" + @echo " dev-backend Run Go backend only" + @echo " dev-frontend Run Vite frontend only" + @echo " install Install all dependencies" + @echo "" + @echo "Building:" + @echo " build Build production binaries" + @echo " build-backend Build Go backend only" + @echo " build-frontend Build React frontend only" + @echo "" + @echo "Testing & Quality:" + @echo " test Run all tests" + @echo " test-backend Run Go tests" + @echo " test-frontend Run frontend tests" + @echo " lint Run all linters" + @echo " lint-backend Run Go linters" + @echo " lint-frontend Run frontend linters" + @echo " fmt Format all code" + @echo " vet Run go vet" + @echo " tidy Run go mod tidy" + @echo "" + @echo "Docker:" + @echo " docker-build Build Docker image" + @echo " docker-run Run Docker container locally" + @echo " docker-push Push Docker image to registry" + @echo "" + @echo "Deployment:" + @echo " deploy Deploy to production (requires config)" + @echo " deploy-staging Deploy to staging" + @echo "" + @echo "Maintenance:" + @echo " clean Clean build artifacts" + @echo " clean-all Clean everything including node_modules" + @echo " generate Generate code (mocks, etc.)" + +# Development +dev: ## Run both backend and frontend in development mode + @echo "Starting development servers..." + @trap 'kill %1; kill %2' INT; \ + $(MAKE) dev-backend & \ + $(MAKE) dev-frontend & \ + wait + +dev-backend: ## Run Go backend + @$(GO) run ./cmd/server + +dev-frontend: ## Run Vite frontend dev server + @cd $(WEB_DIR) && npm run dev + +install: ## Install all dependencies + @echo "Installing Go dependencies..." + @$(GO) mod download + @echo "Installing frontend dependencies..." + @cd $(WEB_DIR) && npm ci + +# Building +build: build-backend build-frontend ## Build everything for production + +build-backend: ## Build Go backend binary + @echo "Building backend $(VERSION)..." + @mkdir -p $(BUILD_DIR) + @CGO_ENABLED=$(CGO_ENABLED) $(GO) build $(GOFLAGS) -o $(BUILD_DIR)/$(APP_NAME) $(SERVER_DIR) + @echo "Backend built: $(BUILD_DIR)/$(APP_NAME)" + +build-frontend: ## Build React frontend + @echo "Building frontend..." + @cd $(WEB_DIR) && npm run build + @echo "Frontend built: $(DIST_DIR)" + +# Testing +test: test-backend test-frontend ## Run all tests + +test-backend: ## Run Go tests + @echo "Running Go tests..." + @$(GO) test -v -race -coverprofile=coverage.out ./... + @$(GO) tool cover -func=coverage.out | tail -1 + +test-frontend: ## Run frontend tests + @echo "Running frontend tests..." + @cd $(WEB_DIR) && npm test -- --run + +# Linting +lint: lint-backend lint-frontend ## Run all linters + +lint-backend: ## Run Go linters + @which golangci-lint > /dev/null || $(GO) install github.com/golangci/golangci-lint/cmd/golangci-lint@latest + @golangci-lint run ./... + +lint-frontend: ## Run frontend linters + @echo "Running frontend linters..." + @cd $(WEB_DIR) && npm run lint + +fmt: ## Format all code + @echo "Formatting Go code..." + @$(GO) fmt ./... + @echo "Formatting frontend code..." + @cd $(WEB_DIR) && npm run format + +vet: ## Run go vet + @$(GO) vet ./... + +tidy: ## Run go mod tidy + @$(GO) mod tidy + +# Docker +docker-build: ## Build Docker image + @echo "Building Docker image $(DOCKER_IMAGE):$(DOCKER_TAG)..." + @docker build -t $(DOCKER_IMAGE):$(DOCKER_TAG) -t $(DOCKER_IMAGE):latest . + @echo "Built: $(DOCKER_IMAGE):$(DOCKER_TAG)" + +docker-run: ## Run Docker container locally + @echo "Running Docker container..." + @docker run --rm -p 8080:8080 \ + -e SERVER_MODE=release \ + --env-file .env.local \ + $(DOCKER_IMAGE):$(DOCKER_TAG) + +docker-push: ## Push Docker image to registry + @echo "Pushing Docker image..." + @docker push $(DOCKER_IMAGE):$(DOCKER_TAG) + @docker push $(DOCKER_IMAGE):latest + +# Deployment +deploy: ## Deploy to production (requires kubectl/helm configured) + @echo "Deploying to production..." + @./deploy/deploy.sh production + +deploy-staging: ## Deploy to staging + @echo "Deploying to staging..." + @./deploy/deploy.sh staging + +# Maintenance +clean: ## Clean build artifacts + @echo "Cleaning build artifacts..." + @rm -rf $(BUILD_DIR) + @rm -rf $(DIST_DIR) + @$(GO) clean -cache + +clean-all: clean ## Clean everything including dependencies + @echo "Cleaning all..." + @rm -rf $(WEB_DIR)/node_modules + @$(GO) clean -modcache + +generate: ## Generate code (mocks, etc.) + @which mockgen > /dev/null || $(GO) install github.com/golang/mock/mockgen@latest + @echo "Generating mocks..." + @$(GO) generate ./... + +# CI/CD helpers +ci-install: ## Install CI dependencies + @$(GO) install github.com/golangci/golangci-lint/cmd/golangci-lint@latest + @$(GO) install github.com/air-verse/air@latest + @$(GO) install github.com/golang/mock/mockgen@latest + +ci-test: ## Run CI tests + @$(MAKE) lint + @$(MAKE) test + +ci-build: ## Build for CI + @$(MAKE) build + +# Release +release-patch: ## Bump patch version + @git tag -a v$(shell semver bump patch) -m "Release patch version" + @git push origin --tags + +release-minor: ## Bump minor version + @git tag -a v$(shell semver bump minor) -m "Release minor version" + @git push origin --tags + +release-major: ## Bump major version + @git tag -a v$(shell semver bump major) -m "Release major version" + @git push origin --tags + +# Database (if using migrations) +db-migrate-up: ## Run database migrations up + @which migrate > /dev/null || $(GO) install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest + @migrate -path ./migrations -database "$(shell grep -A5 'database:' config.yaml | grep 'dsn' | cut -d: -f2- | xargs)" up + +db-migrate-down: ## Run database migrations down + @migrate -path ./migrations -database "$(shell grep -A5 'database:' config.yaml | grep 'dsn' | cut -d: -f2- | xargs)" down + +db-migrate-create: ## Create new migration (usage: make db-migrate-create name=add_users_table) + @migrate create -ext sql -dir ./migrations -seq $(name) + +# Utilities +version: ## Show version info + @echo "Version: $(VERSION)" + @echo "Build Time: $(BUILD_TIME)" + @echo "Git Commit: $(GIT_COMMIT)" + +check-deps: ## Check for outdated dependencies + @$(GO) list -u -m all + @cd $(WEB_DIR) && npm outdated + +# Server management +start: build ## Build and start production server + @./$(BUILD_DIR)/$(APP_NAME) + +stop: ## Stop running server (if using PID file) + @pkill -f $(APP_NAME) || true + +restart: stop start ## Restart server + +logs: ## View server logs (if using systemd) + @journalctl -u $(APP_NAME) -f + +status: ## Check server status + @systemctl status $(APP_NAME) || docker ps | grep $(APP_NAME) + +# SSL/Certificates (using Let's Encrypt) +cert-renew: ## Renew SSL certificates + @certbot renew --nginx + +cert-status: ## Check certificate status + @certbot certificates \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..6827a2f --- /dev/null +++ b/README.md @@ -0,0 +1,358 @@ +# go-first — URPI Web Application + +[![Go Version](https://img.shields.io/badge/Go-1.23-00ADD8?logo=go)](https://golang.org) +[![React Version](https://img.shields.io/badge/React-18.3-61DAFB?logo=react)](https://react.dev) +[![TypeScript](https://img.shields.io/badge/TypeScript-5.5-3178C6?logo=typescript)](https://typescriptlang.org) +[![Tailwind CSS](https://img.shields.io/badge/Tailwind-3.4-06B6D4?logo=tailwindcss)](https://tailwindcss.com) +[![Docker](https://img.shields.io/badge/Docker-Ready-2496ED?logo=docker)](https://docker.com) +[![License](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE) + +> **URPI** — Web Application & Backend Development Experts +> Live at: [https://www.urpi.be](https://www.urpi.be) + +A modern, full-stack web application showcasing URPI's expertise in web application and backend development. Built with Go, React, TypeScript, and deployed on cloud-native infrastructure. + +## ✨ Features + +- **Modern Go Backend** — Clean architecture with Gin, structured logging, and middleware +- **React + TypeScript Frontend** — Vite, Tailwind CSS, and component-driven development +- **RESTful API** — Versioned endpoints for skills, projects, experience, services, and contact +- **Responsive Design** — Mobile-first, accessible, with "go-bananas" inspired bold styling +- **Docker Ready** — Multi-stage builds, docker-compose for local dev and production +- **CI/CD Friendly** — Makefile with all common tasks, GitHub Actions ready +- **Production Hardened** — Security headers, rate limiting, health checks, SSL termination + +## 🚀 Quick Start + +### Prerequisites + +- Go 1.23+ +- Node.js 20+ +- Docker & Docker Compose (for containerized deployment) +- PostgreSQL 16+ & Redis 7+ (or use Docker) + +### Development + +```bash +# Clone and enter +git clone https://github.com/dnoulet/go-first.git +cd go-first + +# Install dependencies +make install + +# Start development servers (backend + frontend) +make dev + +# Or run separately: +make dev-backend # Go backend on :8080 with hot reload +make dev-frontend # Vite frontend on :5173 with HMR +``` + +Visit: +- Frontend: http://localhost:5173 +- Backend API: http://localhost:8080/api/v1 +- Health check: http://localhost:8080/api/v1/health + +### Environment Setup + +```bash +# Copy example env files +cp .env.local.example .env.local +cp .env.production.example .env.production + +# Edit with your values +vim .env.local +``` + +### Database (Docker) + +```bash +# Start only PostgreSQL and Redis +docker-compose up -d postgres redis + +# Run migrations (when implemented) +make db-migrate-up +``` + +## 🏗️ Project Structure + +``` +go-first/ +├── cmd/server/ # Go application entry point +├── internal/ +│ ├── config/ # Configuration loading (YAML + env) +│ ├── handler/ # HTTP handlers (Gin) +│ ├── middleware/ # Custom middleware (logging, CORS, security) +│ ├── model/ # Data models & API responses +│ └── service/ # Business logic (in-memory, swappable for DB) +├── web/ # React frontend +│ ├── src/ +│ │ ├── components/ # UI, layout, sections, forms +│ │ ├── hooks/ # Custom React hooks +│ │ ├── services/ # API client +│ │ ├── types/ # TypeScript types +│ │ └── utils/ # Helpers +│ └── public/ # Static assets +├── deploy/ # Deployment configs +│ ├── nginx.conf # NGINX reverse proxy +│ └── deploy.sh # Deployment script +├── docs/ # Documentation +├── Dockerfile # Multi-stage Docker build +├── docker-compose.yml # Local/production stack +├── Makefile # Build, test, deploy commands +├── config.yaml # Application configuration +└── go.mod # Go dependencies +``` + +## 🛠️ Makefile Commands + +```bash +# Development +make dev # Run both backend & frontend +make dev-backend # Backend only (with air hot reload) +make dev-frontend # Frontend only (Vite) +make install # Install all dependencies + +# Building +make build # Build everything for production +make build-backend # Build Go binary +make build-frontend # Build React app + +# Testing & Quality +make test # Run all tests +make test-backend # Go tests with coverage +make test-frontend # Frontend tests (Vitest) +make lint # Run all linters +make lint-backend # golangci-lint +make lint-frontend # ESLint + Prettier +make fmt # Format all code +make vet # go vet +make tidy # go mod tidy + +# Docker +make docker-build # Build Docker image +make docker-run # Run container locally +make docker-push # Push to registry + +# Deployment +make deploy # Deploy to production +make deploy-staging # Deploy to staging + +# Maintenance +make clean # Clean build artifacts +make clean-all # Clean everything (including node_modules) +make generate # Generate mocks/code +``` + +## 📡 API Endpoints + +All endpoints prefixed with `/api/v1` + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/health` | Health check | +| GET | `/skills` | List all skills (query: `category`) | +| GET | `/skills/:id` | Get single skill | +| GET | `/projects` | List projects (query: `category`, `featured`) | +| GET | `/projects/featured` | Get featured projects | +| GET | `/projects/:id` | Get single project | +| GET | `/experience` | List work experience | +| GET | `/services` | List services offered | +| POST | `/contact` | Submit contact form | + +### Example Responses + +**GET /api/v1/skills** +```json +{ + "success": true, + "data": [ + { + "id": "uuid", + "name": "Go", + "category": "Backend", + "proficiency": 5, + "icon": "go", + "order": 1 + } + ] +} +``` + +**POST /api/v1/contact** +```json +// Request +{ + "name": "John Doe", + "email": "john@example.com", + "subject": "Project Inquiry", + "message": "Hello, I'd like to discuss..." +} + +// Response (201) +{ + "success": true, + "data": { + "id": "uuid", + "name": "John Doe", + "email": "john@example.com", + "subject": "Project Inquiry", + "message": "Hello, I'd like to discuss...", + "status": "new", + "created_at": "2024-01-15T10:30:00Z" + } +} +``` + +## 🎨 Styling — "go-bananas" Design System + +The frontend uses a custom design system inspired by the **go-bananas** aesthetic: + +- **Brand Color**: Vibrant orange (`#ed7315`) — energetic, technical, approachable +- **Typography**: Inter (UI) + Cal Sans (display) + JetBrains Mono (code) +- **Components**: Card-based, subtle animations, glass morphism accents +- **Dark Mode**: Ready (CSS variables, Tailwind dark:) +- **Accessibility**: WCAG AA, focus states, semantic HTML, ARIA labels + +### Tailwind Customization + +```js +// tailwind.config.js - key extensions +colors: { + brand: { 500: '#ed7315', ... }, // Primary orange + dark: { 900: '#0f172a', ... } // Slate dark mode +}, +fontFamily: { + sans: ['Inter', ...], + display: ['Cal Sans', 'Inter', ...], + mono: ['JetBrains Mono', ...] +}, +animation: { + 'fade-in': 'fadeIn 0.5s ease-out', + 'slide-up': 'slideUp 0.6s ease-out', + 'float': 'float 6s ease-in-out infinite', +} +``` + +## 🐳 Docker Deployment + +### Local Production Stack + +```bash +# Build and start all services +docker-compose up -d --build + +# View logs +docker-compose logs -f app + +# Stop +docker-compose down +``` + +### Production Deployment + +```bash +# Configure environment +cp .env.production.example .env.production +# Edit with production values + +# Deploy +make deploy +# Or manually: +./deploy/deploy.sh production +``` + +### Kubernetes (Optional) + +```bash +# Apply manifests +kubectl apply -k deploy/kubernetes/overlays/production + +# Check status +kubectl get pods -n go-first-production +``` + +## 🔧 Configuration + +### config.yaml + +```yaml +server: + host: "0.0.0.0" + port: 8080 + mode: "debug" # debug | release + read_timeout: 10 + write_timeout: 10 + +database: + host: "localhost" + port: 5432 + # ... + +jwt: + secret: "your-secret" + expires_in: 24 # hours + +cors: + allowed_origins: + - "https://www.urpi.be" + # ... +``` + +Environment variables override YAML config. + +## 🧪 Testing + +```bash +# Backend tests +go test -v -race -coverprofile=coverage.out ./... +go tool cover -html=coverage.out + +# Frontend tests +cd web && npm test -- --run --coverage +``` + +## 📦 Dependencies + +### Backend (Go) +- `gin-gonic/gin` — HTTP framework +- `rs/zerolog` — Structured logging +- `joho/godotenv` — Environment loading +- `gopkg.in/yaml.v3` — YAML config + +### Frontend (React) +- `react`, `react-dom`, `react-router-dom` +- `lucide-react` — Icons +- `clsx`, `tailwind-merge` — Class utilities +- `vite` — Build tool +- `typescript`, `eslint`, `prettier` + +## 🤝 Contributing + +1. Fork the repository +2. Create a feature branch: `git checkout -b feature/amazing-feature` +3. Commit changes: `git commit -m 'Add amazing feature'` +4. Push to branch: `git push origin feature/amazing-feature` +5. Open a Pull Request + +### Code Style + +- Go: `gofmt`, `golangci-lint` +- TypeScript: `eslint`, `prettier` (with Tailwind plugin) +- Conventional commits: `feat:`, `fix:`, `docs:`, `refactor:` + +## 📄 License + +MIT License — see [LICENSE](LICENSE) for details. + +## 🌐 URPI + +**URPI** — Web Application & Backend Development +Belgium | [https://www.urpi.be](https://www.urpi.be) | contact@urpi.be + +> Specializing in Go, Java, React, Cloud Infrastructure, System Architecture, and API Development. + +--- + +Built with ❤️ and lots of ☕ by the URPI team. \ No newline at end of file diff --git a/cmd/server/main.go b/cmd/server/main.go new file mode 100644 index 0000000..8a5b106 --- /dev/null +++ b/cmd/server/main.go @@ -0,0 +1,118 @@ +package main + +import ( + "context" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "github.com/dnoulet/go-first/internal/config" + "github.com/dnoulet/go-first/internal/handler" + "github.com/dnoulet/go-first/internal/middleware" + "github.com/dnoulet/go-first/internal/service" + "github.com/gin-gonic/gin" + "github.com/joho/godotenv" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" +) + +func main() { + if err := godotenv.Load(); err != nil { + log.Info().Msg("No .env file found, using config.yaml") + } + + cfg, err := config.Load("config.yaml") + if err != nil { + log.Fatal().Err(err).Msg("Failed to load config") + } + + setupLogging(cfg.Logging) + + if cfg.Server.Mode == "release" { + gin.SetMode(gin.ReleaseMode) + } + + router := setupRouter(cfg) + + srv := &http.Server{ + Addr: cfg.Server.Address(), + Handler: router, + ReadTimeout: cfg.Server.ReadTimeoutDuration(), + WriteTimeout: cfg.Server.WriteTimeoutDuration(), + IdleTimeout: cfg.Server.IdleTimeoutDuration(), + } + + go func() { + log.Info().Str("address", cfg.Server.Address()).Msg("Starting server") + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Fatal().Err(err).Msg("Failed to start server") + } + }() + + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + <-quit + + log.Info().Msg("Shutting down server...") + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + if err := srv.Shutdown(ctx); err != nil { + log.Fatal().Err(err).Msg("Server forced to shutdown") + } + + log.Info().Msg("Server exited") +} + +func setupLogging(cfg config.LoggingConfig) { + level, err := zerolog.ParseLevel(cfg.Level) + if err != nil { + level = zerolog.InfoLevel + } + zerolog.SetGlobalLevel(level) + + if cfg.Format == "console" || cfg.Output == "stdout" { + log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC3339}) + } +} + +func setupRouter(cfg *config.Config) *gin.Engine { + router := gin.New() + + router.Use(middleware.Recovery()) + router.Use(middleware.Logger()) + router.Use(middleware.SecurityHeaders()) + router.Use(middleware.CORS(cfg.CORS.AllowedOrigins)) + + skillService := service.NewSkillService() + projectService := service.NewProjectService() + experienceService := service.NewExperienceService() + serviceService := service.NewServiceService() + emailService := service.NewMockEmailService() + contactService := service.NewContactService(emailService) + + h := handler.NewHandler( + skillService, + projectService, + experienceService, + serviceService, + contactService, + ) + + h.RegisterRoutes(router) + + router.NoRoute(func(c *gin.Context) { + c.JSON(404, gin.H{ + "success": false, + "error": gin.H{ + "code": "NOT_FOUND", + "message": "Route not found", + }, + }) + }) + + return router +} diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000..63ab260 --- /dev/null +++ b/config.yaml @@ -0,0 +1,68 @@ +server: + host: "0.0.0.0" + port: 8081 + mode: "debug" + read_timeout: 10 + write_timeout: 10 + idle_timeout: 60 + +database: + host: "localhost" + port: 5432 + user: "postgres" + password: "postgres" + name: "go_first" + ssl_mode: "disable" + max_open_conns: 25 + max_idle_conns: 5 + conn_max_lifetime: 300 + +redis: + host: "localhost" + port: 6379 + password: "" + db: 0 + +jwt: + secret: "your-super-secret-jwt-key-change-in-production" + expires_in: 24 + refresh_expires_in: 168 + +cors: + allowed_origins: + - "https://www.urpi.be" + - "https://urpi.be" + - "http://localhost:3000" + - "http://localhost:5173" + allowed_methods: + - "GET" + - "POST" + - "PUT" + - "PATCH" + - "DELETE" + - "OPTIONS" + allowed_headers: + - "Origin" + - "Content-Type" + - "Accept" + - "Authorization" + - "X-Requested-With" + allow_credentials: true + max_age: 86400 + +logging: + level: "debug" + format: "console" + output: "stdout" + +email: + smtp_host: "smtp.gmail.com" + smtp_port: 587 + username: "" + password: "" + from_address: "noreply@urpi.be" + from_name: "URPI" + +frontend: + build_path: "./web/dist" + spa_fallback: true \ No newline at end of file diff --git a/deploy/deploy.sh b/deploy/deploy.sh new file mode 100755 index 0000000..1782ad5 --- /dev/null +++ b/deploy/deploy.sh @@ -0,0 +1,132 @@ +#!/bin/bash +# Deployment script for go-first / URPI +# Usage: ./deploy.sh [production|staging] + +set -euo pipefail + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +ENVIRONMENT="${1:-staging}" +PROJECT_NAME="go-first" +DOCKER_IMAGE="ghcr.io/dnoulet/${PROJECT_NAME}" +VERSION=$(git describe --tags --always --dirty 2>/dev/null || echo "dev") + +log_info() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +log_warn() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +check_requirements() { + log_info "Checking requirements..." + + command -v docker >/dev/null 2>&1 || { log_error "docker is required but not installed."; exit 1; } + command -v docker-compose >/dev/null 2>&1 || { log_error "docker-compose is required but not installed."; exit 1; } + + if [[ "$ENVIRONMENT" == "production" ]]; then + command -v kubectl >/dev/null 2>&1 || log_warn "kubectl not found, using docker-compose" + fi +} + +build_and_push() { + log_info "Building Docker image: ${DOCKER_IMAGE}:${VERSION}" + docker build -t "${DOCKER_IMAGE}:${VERSION}" -t "${DOCKER_IMAGE}:latest" . + + log_info "Pushing Docker image..." + docker push "${DOCKER_IMAGE}:${VERSION}" + docker push "${DOCKER_IMAGE}:latest" +} + +deploy_docker_compose() { + log_info "Deploying with docker-compose (${ENVIRONMENT})..." + + ENV_FILE=".env.${ENVIRONMENT}" + if [[ ! -f "$ENV_FILE" ]]; then + log_error "Environment file ${ENV_FILE} not found!" + exit 1 + fi + + # Pull latest images + docker-compose --env-file "$ENV_FILE" pull + + # Start services + docker-compose --env-file "$ENV_FILE" up -d --remove-orphans + + # Wait for health checks + log_info "Waiting for services to be healthy..." + sleep 10 + + # Check health + if docker-compose --env-file "$ENV_FILE" ps | grep -q "unhealthy"; then + log_error "Some services are unhealthy!" + docker-compose --env-file "$ENV_FILE" ps + exit 1 + fi + + log_info "Deployment successful!" +} + +deploy_kubernetes() { + log_info "Deploying to Kubernetes (${ENVIRONMENT})..." + + # Apply Kubernetes manifests + kubectl apply -k "deploy/kubernetes/overlays/${ENVIRONMENT}" + + # Wait for rollout + kubectl rollout status deployment/${PROJECT_NAME} -n ${PROJECT_NAME}-${ENVIRONMENT} --timeout=300s + + log_info "Kubernetes deployment successful!" +} + +run_migrations() { + log_info "Running database migrations..." + + if [[ "$ENVIRONMENT" == "production" ]]; then + # Run migrations in production + docker-compose --env-file ".env.${ENVIRONMENT}" exec -T app ./go-first migrate up + else + docker-compose --env-file ".env.${ENVIRONMENT}" exec -T app ./go-first migrate up + fi +} + +cleanup() { + log_info "Cleaning up old Docker images..." + docker image prune -f +} + +main() { + log_info "Starting deployment for ${ENVIRONMENT} (version: ${VERSION})" + + check_requirements + + if [[ "$ENVIRONMENT" != "production" && "$ENVIRONMENT" != "staging" ]]; then + log_error "Invalid environment. Use 'production' or 'staging'" + exit 1 + fi + + build_and_push + + if command -v kubectl >/dev/null 2>&1 && [[ "$ENVIRONMENT" == "production" ]]; then + deploy_kubernetes + else + deploy_docker_compose + fi + + run_migrations + cleanup + + log_info "Deployment completed successfully!" + log_info "Application available at: https://www.urpi.be" +} + +main "$@" \ No newline at end of file diff --git a/deploy/nginx.conf b/deploy/nginx.conf new file mode 100644 index 0000000..8913109 --- /dev/null +++ b/deploy/nginx.conf @@ -0,0 +1,186 @@ +# NGINX Configuration for go-first / URPI +# Deploy to /etc/nginx/nginx.conf + +user nginx; +worker_processes auto; +error_log /var/log/nginx/error.log warn; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; + use epoll; + multi_accept on; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + # Logging + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for" ' + 'rt=$request_time uct="$upstream_connect_time" ' + 'uht="$upstream_header_time" urt="$upstream_response_time"'; + + access_log /var/log/nginx/access.log main; + + # Performance + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + server_tokens off; + + # Gzip + gzip on; + gzip_vary on; + gzip_proxied any; + gzip_comp_level 6; + gzip_min_length 1000; + gzip_types + text/plain + text/css + text/xml + text/javascript + application/json + application/javascript + application/xml+rss + application/atom+xml + image/svg+xml; + + # Brotli (if available) + # brotli on; + # brotli_comp_level 6; + # brotli_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; + + # Rate limiting + limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s; + limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m; + + # Upstream + upstream go_first_backend { + server app:8080; + keepalive 32; + } + + # HTTP -> HTTPS redirect + server { + listen 80; + listen [::]:80; + server_name www.urpi.be urpi.be; + + # ACME challenge for Let's Encrypt + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + # Redirect all other traffic to HTTPS + location / { + return 301 https://www.urpi.be$request_uri; + } + } + + # HTTPS server + server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + server_name www.urpi.be urpi.be; + + # SSL Configuration + ssl_certificate /etc/nginx/ssl/fullchain.pem; + ssl_certificate_key /etc/nginx/ssl/privkey.pem; + ssl_session_timeout 1d; + ssl_session_cache shared:SSL:10m; + ssl_session_tickets off; + + # Modern SSL config + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; + ssl_prefer_server_ciphers off; + + # HSTS + add_header Strict-Transport-Security "max-age=31536000" always; + + # OCSP Stapling + ssl_stapling on; + ssl_stapling_verify on; + resolver 1.1.1.1 8.8.8.8 valid=300s; + resolver_timeout 5s; + + # Security headers + add_header X-Frame-Options "DENY" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self'; frame-ancestors 'none';" always; + + # Frontend static files + root /var/www/html; + index index.html; + + # Cache static assets + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + try_files $uri =404; + } + + # API routes + location /api/ { + limit_req zone=api burst=20 nodelay; + + proxy_pass http://go_first_backend; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_cache_bypass $http_upgrade; + + # Timeouts + proxy_connect_timeout 5s; + proxy_send_timeout 30s; + proxy_read_timeout 30s; + + # Buffering + proxy_buffering on; + proxy_buffer_size 4k; + proxy_buffers 8 4k; + } + + # Health check endpoint (no rate limit) + location /api/v1/health { + proxy_pass http://go_first_backend; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + access_log off; + } + + # SPA fallback - serve index.html for all other routes + location / { + try_files $uri $uri/ /index.html; + + # Cache HTML for short time + add_header Cache-Control "no-cache, must-revalidate"; + } + + # Block access to hidden files + location ~ /\. { + deny all; + access_log off; + log_not_found off; + } + + # Block access to backup/config files + location ~* \.(env|log|conf|bak|backup|sql)$ { + deny all; + access_log off; + log_not_found off; + } + } +} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..1eeebcb --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,95 @@ +version: '3.8' + +services: + app: + build: + context: . + dockerfile: Dockerfile + container_name: go-first-app + ports: + - "8080:8080" + environment: + - SERVER_MODE=release + - SERVER_HOST=0.0.0.0 + - SERVER_PORT=8080 + env_file: + - .env.production + volumes: + - ./config.yaml:/app/config.yaml:ro + - app_uploads:/app/uploads + - app_logs:/app/logs + restart: unless-stopped + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/api/v1/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 10s + networks: + - go-first-network + + postgres: + image: postgres:16-alpine + container_name: go-first-postgres + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=postgres + - POSTGRES_DB=go_first + volumes: + - postgres_data:/var/lib/postgresql/data + - ./migrations:/docker-entrypoint-initdb.d:ro + ports: + - "5432:5432" + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - go-first-network + + redis: + image: redis:7-alpine + container_name: go-first-redis + command: redis-server --appendonly yes + volumes: + - redis_data:/data + ports: + - "6379:6379" + restart: unless-stopped + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - go-first-network + + nginx: + image: nginx:alpine + container_name: go-first-nginx + ports: + - "80:80" + - "443:443" + volumes: + - ./deploy/nginx.conf:/etc/nginx/nginx.conf:ro + - ./deploy/ssl:/etc/nginx/ssl:ro + - ./web/dist:/var/www/html:ro + - nginx_cache:/var/cache/nginx + depends_on: + - app + restart: unless-stopped + networks: + - go-first-network + +volumes: + postgres_data: + redis_data: + app_uploads: + app_logs: + nginx_cache: + +networks: + go-first-network: + driver: bridge \ No newline at end of file diff --git a/docs/API.md b/docs/API.md new file mode 100644 index 0000000..ef7956b --- /dev/null +++ b/docs/API.md @@ -0,0 +1,405 @@ +# API Documentation — go-first / URPI + +Base URL: `https://www.urpi.be/api/v1` +Local: `http://localhost:8080/api/v1` + +All responses follow a consistent envelope format. + +--- + +## Response Format + +### Success +```json +{ + "success": true, + "data": {}, + "meta": { + "page": 1, + "page_size": 20, + "total": 100, + "total_pages": 5 + } +} +``` + +### Error +```json +{ + "success": false, + "error": { + "code": "ERROR_CODE", + "message": "Human readable message", + "details": "Optional technical details" + } +} +``` + +--- + +## Common Error Codes + +| Code | HTTP Status | Description | +|------|-------------|-------------| +| `VALIDATION_ERROR` | 400 | Request body validation failed | +| `INVALID_ID` | 400 | UUID format invalid | +| `NOT_FOUND` | 404 | Resource not found | +| `UNAUTHORIZED` | 401 | Authentication required | +| `FORBIDDEN` | 403 | Insufficient permissions | +| `CONFLICT` | 409 | Resource conflict | +| `RATE_LIMITED` | 429 | Too many requests | +| `INTERNAL_ERROR` | 500 | Server error | +| `SERVICE_UNAVAILABLE` | 503 | Service temporarily unavailable | + +--- + +## Endpoints + +### Health Check + +**GET** `/health` + +No authentication required. + +#### Response +```json +{ + "success": true, + "data": { + "status": "ok", + "service": "go-first", + "version": "1.0.0" + } +} +``` + +--- + +### Skills + +#### List Skills + +**GET** `/skills` + +| Query Param | Type | Description | +|-------------|------|-------------| +| `category` | string | Filter by category (Backend, Frontend, Architecture, DevOps, Database, API) | + +#### Response +```json +{ + "success": true, + "data": [ + { + "id": "uuid", + "name": "Go", + "category": "Backend", + "proficiency": 5, + "icon": "go", + "order": 1, + "created_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-01-01T00:00:00Z" + } + ] +} +``` + +#### Get Skill + +**GET** `/skills/:id` + +| Path Param | Type | Description | +|------------|------|-------------| +| `id` | uuid | Skill UUID | + +#### Response +```json +{ + "success": true, + "data": { + "id": "uuid", + "name": "Go", + "category": "Backend", + "proficiency": 5, + "icon": "go", + "order": 1, + "created_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-01-01T00:00:00Z" + } +} +``` + +--- + +### Projects + +#### List Projects + +**GET** `/projects` + +| Query Param | Type | Description | +|-------------|------|-------------| +| `category` | string | Filter by category | +| `featured` | boolean | Filter featured projects only | + +#### Response +```json +{ + "success": true, + "data": [ + { + "id": "uuid", + "title": "E-Commerce Platform", + "description": "Full description...", + "short_desc": "Scalable e-commerce platform...", + "tech_stack": ["Go", "React", "PostgreSQL", "Redis", "Kubernetes"], + "image_url": "/images/projects/ecommerce.jpg", + "demo_url": "https://demo.urpi.be/ecommerce", + "repo_url": "https://github.com/urpi/ecommerce-platform", + "category": "Full Stack", + "featured": true, + "order": 1, + "created_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-01-01T00:00:00Z" + } + ] +} +``` + +#### Get Featured Projects + +**GET** `/projects/featured` + +Returns only projects with `featured: true`. + +#### Get Project + +**GET** `/projects/:id` + +| Path Param | Type | Description | +|------------|------|-------------| +| `id` | uuid | Project UUID | + +--- + +### Experience + +#### List Experience + +**GET** `/experience` + +Returns all work experience entries ordered by `order` field. + +#### Response +```json +{ + "success": true, + "data": [ + { + "id": "uuid", + "company": "URPI", + "role": "Founder & Lead Developer", + "location": "Belgium", + "start_date": "2020-01-01T00:00:00Z", + "end_date": null, + "current": true, + "description": "Founded URPI to provide web application...", + "tech_stack": ["Go", "Java", "React", "Kubernetes", "AWS"], + "order": 1, + "created_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-01-01T00:00:00Z" + } + ] +} +``` + +--- + +### Services + +#### List Services + +**GET** `/services` + +Returns all active services offered. + +#### Response +```json +{ + "success": true, + "data": [ + { + "id": "uuid", + "title": "Web Application Development", + "description": "Custom web applications built with modern technologies...", + "icon": "code", + "features": [ + "React/Next.js Frontend", + "Go/Java Backend", + "REST/GraphQL APIs" + ], + "order": 1, + "active": true, + "created_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-01-01T00:00:00Z" + } + ] +} +``` + +--- + +### Contact + +#### Submit Contact Form + +**POST** `/contact` + +Content-Type: `application/json` + +| Field | Type | Required | Validation | +|-------|------|----------|------------| +| `name` | string | Yes | 2-100 chars | +| `email` | string | Yes | Valid email format | +| `subject` | string | Yes | 5-200 chars | +| `message` | string | Yes | 10-5000 chars | + +#### Request +```json +{ + "name": "John Doe", + "email": "john@example.com", + "subject": "Project Inquiry", + "message": "Hello, I'd like to discuss a potential project..." +} +``` + +#### Response (201 Created) +```json +{ + "success": true, + "data": { + "id": "uuid", + "name": "John Doe", + "email": "john@example.com", + "subject": "Project Inquiry", + "message": "Hello, I'd like to discuss a potential project...", + "status": "new", + "created_at": "2024-01-15T10:30:00Z", + "updated_at": "2024-01-15T10:30:00Z" + } +} +``` + +#### Validation Error (400) +```json +{ + "success": false, + "error": { + "code": "VALIDATION_ERROR", + "message": "Invalid request body", + "details": "Key: 'ContactForm.Email' Error:Field validation for 'Email' failed on the 'email' tag" + } +} +``` + +--- + +## Rate Limiting + +| Endpoint | Limit | +|----------|-------| +| `/api/v1/*` (general) | 10 req/s per IP | +| `/api/v1/contact` | 5 req/min per IP | +| `/api/v1/health` | No limit | + +Headers returned: +- `X-RateLimit-Limit` +- `X-RateLimit-Remaining` +- `X-RateLimit-Reset` + +--- + +## CORS + +Allowed origins (configured in `config.yaml`): +- `https://www.urpi.be` +- `https://urpi.be` +- `http://localhost:3000` +- `http://localhost:5173` + +Allowed methods: `GET, POST, PUT, PATCH, DELETE, OPTIONS` +Allowed headers: `Origin, Content-Type, Accept, Authorization, X-Requested-With` +Credentials: `true` + +--- + +## Versioning + +API version in URL path: `/api/v1/` + +Breaking changes will result in `/api/v2/` etc. +Non-breaking additions (new fields, new endpoints) may be added to v1. + +--- + +## Authentication + +Currently **no authentication required** for public endpoints. + +Future plans: +- JWT-based auth for admin endpoints +- API keys for programmatic access +- OAuth2 for third-party integrations + +--- + +## SDKs / Clients + +### JavaScript/TypeScript +```typescript +const API_BASE = '/api/v1'; + +async function fetchAPI(endpoint: string, options: RequestInit = {}): Promise { + const res = await fetch(`${API_BASE}${endpoint}`, { + headers: { 'Content-Type': 'application/json', ...options.headers }, + ...options, + }); + const data = await res.json(); + if (!res.ok || !data.success) throw new Error(data.error?.message || 'Error'); + return data.data; +} + +// Usage +const skills = await fetchAPI('/skills'); +const project = await fetchAPI('/projects/uuid'); +await fetchAPI('/contact', { + method: 'POST', + body: JSON.stringify({ name, email, subject, message }) +}); +``` + +### Go +```go +type APIResponse struct { + Success bool `json:"success"` + Data interface{} `json:"data,omitempty"` + Error *APIError `json:"error,omitempty"` +} + +func GetSkills(ctx context.Context) ([]Skill, error) { + req, _ := http.NewRequestWithContext(ctx, "GET", "https://www.urpi.be/api/v1/skills", nil) + resp, err := http.DefaultClient.Do(req) + // ... parse response +} +``` + +--- + +## Changelog + +### v1.0.0 (2024-01-15) +- Initial API release +- Skills, Projects, Experience, Services, Contact endpoints +- Rate limiting and CORS configuration +- Health check endpoint \ No newline at end of file diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md new file mode 100644 index 0000000..54a86d2 --- /dev/null +++ b/docs/DEPLOYMENT.md @@ -0,0 +1,425 @@ +# Deployment Guide — go-first / URPI + +This guide covers deploying the go-first application to production at **https://www.urpi.be**. + +--- + +## 🏗️ Architecture Overview + +``` +┌─────────────┐ ┌─────────────┐ ┌─────────────┐ +│ Users │────▶│ NGINX │────▶│ Go App │ +│ (HTTPS) │ │ (SSL/TLS) │ │ (Port 8080)│ +└─────────────┘ └─────────────┘ └──────┬──────┘ + │ + ┌─────────────┐ ┌──────▼──────┐ + │ PostgreSQL │◀───▶│ Redis │ + │ (Port 5432)│ │ (Port 6379)│ + └─────────────┘ └─────────────┘ +``` + +--- + +## 📋 Prerequisites + +### Server Requirements +- **OS**: Ubuntu 22.04+ / Debian 12+ / Alpine Linux +- **CPU**: 2+ vCPUs +- **RAM**: 4GB+ (2GB minimum for app + DB) +- **Disk**: 20GB+ SSD +- **Network**: Public IPv4/IPv6, ports 80, 443, 22 + +### Software +- Docker 24+ & Docker Compose 2+ +- Git +- Certbot (Let's Encrypt) +- (Optional) kubectl for Kubernetes + +--- + +## 🔧 Server Setup + +### 1. Initial Server Hardening + +```bash +# Update system +sudo apt update && sudo apt upgrade -y + +# Create deploy user +sudo adduser deploy --gecos "" --disabled-password +sudo usermod -aG docker deploy +sudo usermod -aG sudo deploy + +# Setup SSH keys (from your local machine) +ssh-copy-id deploy@your-server-ip + +# Configure firewall +sudo ufw allow 22/tcp # SSH +sudo ufw allow 80/tcp # HTTP +sudo ufw allow 443/tcp # HTTPS +sudo ufw enable +``` + +### 2. Install Docker + +```bash +# Install Docker +curl -fsSL https://get.docker.com | sh + +# Install Docker Compose +sudo apt install -y docker-compose-plugin + +# Verify +docker --version +docker compose version +``` + +### 3. Install Certbot + +```bash +sudo apt install -y certbot python3-certbot-nginx +``` + +--- + +## 📦 Application Deployment + +### 1. Clone Repository + +```bash +cd /opt +sudo git clone https://github.com/dnoulet/go-first.git +sudo chown -R deploy:deploy go-first +cd go-first +``` + +### 2. Configure Environment + +```bash +# Copy production template +cp .env.production.example .env.production + +# Edit with your values (use strong passwords!) +vim .env.production + +# Required changes: +# - DATABASE_PASSWORD: Strong random password +# - JWT_SECRET: 32+ char random string +# - EMAIL_*: Your SMTP credentials +``` + +### 3. Generate SSL Certificates + +```bash +# Stop any service on port 80 +sudo systemctl stop nginx 2>/dev/null || true + +# Get certificates +sudo certbot certonly --standalone \ + -d www.urpi.be \ + -d urpi.be \ + --email contact@urpi.be \ + --agree-tos --non-interactive + +# Certificates will be at: +# /etc/letsencrypt/live/www.urpi.be/fullchain.pem +# /etc/letsencrypt/live/www.urpi.be/privkey.pem +``` + +### 4. Setup NGINX SSL Directory + +```bash +sudo mkdir -p /opt/go-first/deploy/ssl +sudo cp /etc/letsencrypt/live/www.urpi.be/fullchain.pem /opt/go-first/deploy/ssl/ +sudo cp /etc/letsencrypt/live/www.urpi.be/privkey.pem /opt/go-first/deploy/ssl/ +sudo chown -R deploy:deploy /opt/go-first/deploy/ssl +``` + +### 5. Deploy with Docker Compose + +```bash +cd /opt/go-first + +# Build and start +docker compose --env-file .env.production up -d --build + +# Verify +docker compose --env-file .env.production ps +docker compose --env-file .env.production logs -f app +``` + +### 6. Verify Deployment + +```bash +# Health check +curl -f https://www.urpi.be/api/v1/health + +# Check frontend +curl -f https://www.urpi.be/ | grep -c "URPI" +``` + +--- + +## 🔐 SSL Certificate Renewal + +### Automatic Renewal (systemd timer) + +```bash +# Check certbot timer +sudo systemctl status certbot.timer + +# Test renewal +sudo certbot renew --dry-run +``` + +### Manual Renewal Hook + +```bash +# Create renewal hook +sudo tee /etc/letsencrypt/renewal-hooks/deploy/nginx-reload.sh > /dev/null <<'EOF' +#!/bin/bash +cp /etc/letsencrypt/live/www.urpi.be/fullchain.pem /opt/go-first/deploy/ssl/ +cp /etc/letsencrypt/live/www.urpi.be/privkey.pem /opt/go-first/deploy/ssl/ +docker compose -f /opt/go-first/docker-compose.yml --env-file /opt/go-first/.env.production exec nginx nginx -s reload +EOF + +sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/nginx-reload.sh +``` + +--- + +## 🗄️ Database Management + +### Backup + +```bash +# Create backup script +cat > /opt/go-first/backup.sh <<'EOF' +#!/bin/bash +set -euo pipefail + +BACKUP_DIR="/opt/backups/go-first" +DATE=$(date +%Y%m%d_%H%M%S) +mkdir -p "$BACKUP_DIR" + +docker compose -f /opt/go-first/docker-compose.yml --env-file /opt/go-first/.env.production \ + exec -T postgres pg_dump -U postgres go_first | gzip > "$BACKUP_DIR/go_first_$DATE.sql.gz" + +# Keep last 30 days +find "$BACKUP_DIR" -name "*.sql.gz" -mtime +30 -delete + +echo "Backup saved to $BACKUP_DIR/go_first_$DATE.sql.gz" +EOF + +chmod +x /opt/go-first/backup.sh + +# Add to crontab (daily at 2 AM) +(crontab -l 2>/dev/null; echo "0 2 * * * /opt/go-first/backup.sh") | crontab - +``` + +### Restore + +```bash +# Stop app +docker compose -f /opt/go-first/docker-compose.yml --env-file /opt/go-first/.env.production stop app + +# Restore +gunzip -c /opt/backups/go-first/go_first_20240115_020000.sql.gz | \ + docker compose -f /opt/go-first/docker-compose.yml --env-file /opt/go-first/.env.production exec -T postgres psql -U postgres go_first + +# Start app +docker compose -f /opt/go-first/docker-compose.yml --env-file /opt/go-first/.env.production start app +``` + +--- + +## 📊 Monitoring & Logging + +### Log Aggregation (Optional: Loki + Promtail) + +```yaml +# Add to docker-compose.yml +services: + promtail: + image: grafana/promtail:2.9 + volumes: + - /var/log:/var/log + - /opt/go-first/deploy/promtail.yml:/etc/promtail/config.yml + depends_on: + - loki + + loki: + image: grafana/loki:2.9 + volumes: + - loki_data:/loki +``` + +### Health Checks + +```bash +# Application health +curl https://www.urpi.be/api/v1/health + +# Database +docker compose exec postgres pg_isready -U postgres + +# Redis +docker compose exec redis redis-cli ping + +# NGINX +docker compose exec nginx nginx -t +``` + +### Metrics (Optional: Prometheus + Grafana) + +Add to `docker-compose.yml`: +```yaml + prometheus: + image: prom/prometheus:v2.47 + volumes: + - ./deploy/prometheus.yml:/etc/prometheus/prometheus.yml + - prometheus_data:/prometheus + ports: + - "9090:9090" + + grafana: + image: grafana/grafana:10.2 + ports: + - "3000:3000" + volumes: + - grafana_data:/var/lib/grafana +``` + +--- + +## 🔄 Updates & Rollbacks + +### Update Application + +```bash +cd /opt/go-first + +# Pull latest code +git pull origin main + +# Rebuild and deploy +docker compose --env-file .env.production up -d --build --remove-orphans + +# Verify +curl -f https://www.urpi.be/api/v1/health +``` + +### Rollback + +```bash +# List images +docker images ghcr.io/dnoulet/go-first + +# Rollback to previous tag +docker tag ghcr.io/dnoulet/go-first:previous-tag ghcr.io/dnoulet/go-first:latest +docker compose --env-file .env.production up -d --no-deps app +``` + +### Zero-Downtime Deploy (with multiple replicas) + +```yaml +# In docker-compose.yml, add: +deploy: + replicas: 2 + update_config: + parallelism: 1 + delay: 10s + order: start-first + restart_policy: + condition: on-failure +``` + +--- + +## 🛡️ Security Checklist + +- [ ] Strong DATABASE_PASSWORD (32+ chars) +- [ ] Strong JWT_SECRET (32+ chars, random) +- [ ] SMTP credentials secured +- [ ] Firewall: only 22, 80, 443 open +- [ ] SSH key-only authentication +- [ ] Fail2ban configured for SSH +- [ ] Regular security updates (`apt update && apt upgrade`) +- [ ] Docker images scanned for vulnerabilities (`docker scout cves`) +- [ ] NGINX security headers enabled +- [ ] Rate limiting configured +- [ ] CSP headers configured +- [ ] HSTS enabled +- [ ] Database not exposed publicly +- [ ] Redis not exposed publicly +- [ ] Backup encryption at rest +- [ ] Audit logging enabled + +--- + +## 🚨 Troubleshooting + +### Application Won't Start + +```bash +# Check logs +docker compose --env-file .env.production logs app + +# Common issues: +# - Port 8080 already in use +# - Database connection failed +# - Config file missing +# - Permission denied on volumes +``` + +### 502 Bad Gateway + +```bash +# Check if app is healthy +docker compose --env-file .env.production ps app + +# Check NGINX config +docker compose --env-file .env.production exec nginx nginx -t + +# Check upstream +docker compose --env-file .env.production exec nginx curl -f http://app:8080/api/v1/health +``` + +### SSL Certificate Issues + +```bash +# Check certificate validity +openssl x509 -in /etc/letsencrypt/live/www.urpi.be/fullchain.pem -text -noout + +# Check NGINX SSL config +docker compose --env-file .env.production exec nginx nginx -T | grep ssl +``` + +### Database Connection Issues + +```bash +# Test connection +docker compose --env-file .env.production exec postgres psql -U postgres -d go_first -c "SELECT 1;" + +# Check credentials in .env.production match docker-compose.yml +``` + +--- + +## 📞 Support + +For deployment issues: +- Check logs: `docker compose logs -f` +- Review this guide +- Contact: contact@urpi.be + +--- + +## 📝 Maintenance Windows + +Recommended: **Sunday 02:00-04:00 UTC** + +- System updates +- Docker image updates +- Database maintenance +- Certificate renewal verification \ No newline at end of file diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md new file mode 100644 index 0000000..026bf9b --- /dev/null +++ b/docs/DEVELOPMENT.md @@ -0,0 +1,596 @@ +# Development Guide — go-first / URPI + +This guide covers local development setup, workflows, and best practices. + +--- + +## 🛠️ Prerequisites + +### Required Tools +| Tool | Version | Install | +|------|---------|---------| +| Go | 1.23+ | `brew install go` / `apt install golang` | +| Node.js | 20+ | `nvm install 20` / `fnm install 20` | +| Docker | 24+ | [docker.com](https://docker.com) | +| Git | 2.40+ | `brew install git` / `apt install git` | + +### Recommended Tools +- **IDE**: VS Code with Go + TypeScript extensions +- **API Client**: HTTPie, Postman, or VS Code REST Client +- **Database**: TablePlus, DBeaver, or pgAdmin +- **Terminal**: Warp, Ghostty, or Kitty + +--- + +## 🚀 Getting Started + +### 1. Clone & Setup + +```bash +git clone https://github.com/dnoulet/go-first.git +cd go-first + +# Install all dependencies +make install + +# This runs: +# - go mod download +# - cd web && npm ci +``` + +### 2. Start Development Databases + +```bash +# Start PostgreSQL + Redis only +docker compose up -d postgres redis + +# Verify +docker compose ps +docker compose exec postgres pg_isready -U postgres +docker compose exec redis redis-cli ping +``` + +### 3. Configure Local Environment + +```bash +cp .env.local.example .env.local + +# Edit if needed (defaults work for Docker databases) +vim .env.local +``` + +### 4. Run Development Servers + +```bash +# Option A: Both together (recommended) +make dev + +# Option B: Separate terminals +# Terminal 1 - Backend +make dev-backend + +# Terminal 2 - Frontend +make dev-frontend +``` + +### 5. Verify + +| Service | URL | +|---------|-----| +| Frontend | http://localhost:5173 | +| Backend API | http://localhost:8080/api/v1 | +| Health Check | http://localhost:8080/api/v1/health | +| PostgreSQL | localhost:5432 | +| Redis | localhost:6379 | + +--- + +## 🔄 Development Workflow + +### Backend Development + +```bash +# Hot reload with air (already configured in .air.toml) +make dev-backend + +# Or manually: +go run ./cmd/server +``` + +#### Adding a New Endpoint + +1. **Model** — Add types in `internal/model/models.go` +2. **Service** — Create service in `internal/service/` +3. **Handler** — Add handler in `internal/handler/handler.go` +4. **Routes** — Register in `handler.RegisterRoutes()` +5. **Tests** — Add tests in `*_test.go` files + +### Frontend Development + +```bash +# Hot module replacement (Vite) +make dev-frontend + +# Or manually: +cd web && npm run dev +``` + +#### Adding a New Page/Section + +1. **Types** — Add to `web/src/types/index.ts` +2. **API** — Add endpoint to `web/src/services/api.ts` +3. **Hook** — Create custom hook in `web/src/hooks/` +4. **Component** — Create in `web/src/components/sections/` +5. **Register** — Import and add to `web/src/App.tsx` + +### Code Generation + +```bash +# Generate mocks for testing +make generate + +# This runs: go generate ./... +# Uses: github.com/golang/mock/mockgen +``` + +--- + +## 🧪 Testing + +### Backend Tests + +```bash +# Run all tests with coverage +make test-backend + +# Run specific package +go test -v ./internal/service/... + +# Run with race detector +go test -race ./... + +# Generate coverage report +go test -coverprofile=coverage.out ./... +go tool cover -html=coverage.out -o coverage.html +``` + +### Frontend Tests + +```bash +# Run tests +make test-frontend + +# Or manually: +cd web && npm test + +# With UI +cd web && npm test -- --ui + +# Coverage +cd web && npm test -- --coverage +``` + +### E2E Tests (Optional) + +```bash +# Install Playwright +cd web && npx playwright install + +# Run E2E tests +cd web && npm run test:e2e +``` + +--- + +## 🔍 Code Quality + +### Linting + +```bash +# All linters +make lint + +# Backend only +make lint-backend +# Runs: golangci-lint run ./... + +# Frontend only +make lint-frontend +# Runs: eslint + prettier check +``` + +### Formatting + +```bash +# Format all code +make fmt + +# Backend +go fmt ./... +goimports -w . + +# Frontend +cd web && npm run format +``` + +### Pre-commit Hooks + +```bash +# Install pre-commit +pip install pre-commit +pre-commit install + +# Run manually +pre-commit run --all-files +``` + +#### `.pre-commit-config.yaml` +```yaml +repos: + - repo: https://github.com/golangci/golangci-lint + rev: v1.56.0 + hooks: + - id: golangci-lint + + - repo: https://github.com/pre-commit/mirrors-prettier + rev: v3.1.0 + hooks: + - id: prettier + types: [typescript, json, yaml, markdown] + + - repo: local + hooks: + - id: go-fmt + name: go fmt + entry: go fmt ./... + language: system + types: [go] +``` + +--- + +## 🗃️ Database Migrations + +### Creating Migrations + +```bash +# Create new migration +make db-migrate-create name=add_users_table + +# This creates: +# migrations/000001_add_users_table.up.sql +# migrations/000001_add_users_table.down.sql +``` + +### Running Migrations + +```bash +# Apply all pending +make db-migrate-up + +# Rollback last +make db-migrate-down + +# Check status +migrate -path ./migrations -database "$DSN" version +``` + +### Migration Best Practices + +- **Up migrations**: Add columns, tables, indexes +- **Down migrations**: Reverse exactly (drop what you added) +- **Never modify** applied migrations +- **Test** both up and down locally +- **Name clearly**: `add_user_email_index`, not `fix_db` + +--- + +## 🐳 Docker Development + +### Build Locally + +```bash +# Build production image +make docker-build + +# Run production image locally +make docker-run + +# Or with docker-compose (includes DB) +docker compose up -d --build +``` + +### Debugging in Container + +```bash +# Run with shell +docker run -it --rm -p 8080:8080 ghcr.io/dnoulet/go-first:latest sh + +# Or attach to running +docker exec -it go-first-app sh +``` + +### Development with Docker Compose Override + +Create `docker-compose.override.yml`: +```yaml +services: + app: + build: + context: . + dockerfile: Dockerfile.dev + volumes: + - .:/app + - /app/web/node_modules + command: air -c .air.toml + environment: + - SERVER_MODE=debug +``` + +--- + +## 🎨 Frontend Architecture + +### Component Structure + +``` +web/src/components/ +├── ui/ # Reusable primitives (Button, Card, Input, Badge) +├── layout/ # Page structure (Header, Footer) +├── sections/ # Page sections (Hero, Services, Projects, etc.) +└── forms/ # Form-specific components +``` + +### State Management + +- **Server state**: `useApi` hook (React Query pattern) +- **Local state**: `useState`, `useReducer` +- **Forms**: Controlled components with validation +- **No global store** (Redux/Zustand) — keep it simple + +### Styling + +- **Tailwind CSS** utility-first +- **Custom design system** in `tailwind.config.js` +- **CSS variables** for theming (dark mode ready) +- **Component variants** via `clsx` + `tailwind-merge` + +### Adding Icons + +```bash +# Use lucide-react (already installed) +import { IconName } from 'lucide-react' + +# Or add to public/ and use +``` + +--- + +## 🔧 Backend Architecture + +### Clean Architecture Layers + +``` +cmd/server/ # Entry point, wiring +internal/ +├── config/ # Configuration (YAML + env) +├── handler/ # HTTP layer (Gin) +├── middleware/ # Cross-cutting concerns +├── model/ # Domain models, DTOs +└── service/ # Business logic +``` + +### Dependency Injection + +Services are instantiated in `main.go` and passed to handlers: + +```go +skillService := service.NewSkillService() +h := handler.NewHandler(skillService, ...) +``` + +### Adding Database Support + +1. Add DB config to `config.yaml` +2. Create `internal/repository/` package +3. Implement interfaces in `service/` +4. Use `database/sql` or `sqlx` or `gorm` +5. Add migrations + +### Middleware Chain + +```go +router.Use( + middleware.Recovery(), // Panic recovery + middleware.Logger(), // Request logging + middleware.SecurityHeaders(), // Security headers + middleware.CORS(...), // CORS +) +``` + +--- + +## 📝 Git Workflow + +### Branch Naming + +| Type | Pattern | Example | +|------|---------|---------| +| Feature | `feat/short-description` | `feat/contact-form-validation` | +| Fix | `fix/short-description` | `fix/cors-header-missing` | +| Docs | `docs/short-description` | `docs/update-api-docs` | +| Refactor | `refactor/short-description` | `refactor/service-layer` | +| Chore | `chore/short-description` | `chore/update-dependencies` | + +### Commit Messages + +Follow [Conventional Commits](https://www.conventionalcommits.org/): + +``` +feat: add contact form validation +fix: resolve CORS issue on production +docs: update API documentation +refactor: extract service interfaces +chore: update Go dependencies +``` + +### Pull Requests + +1. Create feature branch from `main` +2. Make changes with tests +3. Run `make lint && make test` +4. Push and open PR +5. Require: CI passing, 1 approval +6. Squash merge to `main` + +--- + +## 🐛 Debugging + +### Backend Debugging + +```bash +# Delve debugger +dlv debug ./cmd/server + +# VS Code launch.json +{ + "type": "go", + "request": "launch", + "mode": "debug", + "program": "${workspaceFolder}/cmd/server", + "env": { + "SERVER_MODE": "debug" + } +} +``` + +### Frontend Debugging + +```bash +# React DevTools (browser extension) +# Redux DevTools (if using) + +# VS Code debug config: +{ + "type": "chrome", + "request": "launch", + "url": "http://localhost:5173", + "webRoot": "${workspaceFolder}/web/src" +} +``` + +### Database Debugging + +```bash +# Connect to PostgreSQL +docker compose exec postgres psql -U postgres -d go_first + +# Common queries +\dt # List tables +\d users # Describe table +SELECT * FROM skills; # Query data +``` + +--- + +## 📦 Dependency Management + +### Go Dependencies + +```bash +# Add dependency +go get github.com/new/package + +# Update all +go get -u ./... +go mod tidy + +# Check outdated +make check-deps +``` + +### Frontend Dependencies + +```bash +# Add dependency +cd web && npm install package-name + +# Add dev dependency +cd web && npm install -D package-name + +# Update +cd web && npm update + +# Audit +cd web && npm audit +``` + +--- + +## 🚀 Performance Tips + +### Backend +- Use connection pooling (configured in config) +- Add Redis caching for expensive queries +- Enable gzip compression (NGINX) +- Use `pprof` for profiling: `import _ "net/http/pprof"` + +### Frontend +- Code splitting (Vite does this automatically) +- Lazy load sections: `const Section = lazy(() => import('./Section'))` +- Optimize images (WebP, proper sizing) +- Use `React.memo` for expensive components + +--- + +## 🔐 Environment Variables + +### Required for Development + +| Variable | Description | Default | +|----------|-------------|---------| +| `SERVER_MODE` | `debug` or `release` | `debug` | +| `DATABASE_HOST` | PostgreSQL host | `localhost` | +| `DATABASE_PASSWORD` | PostgreSQL password | `postgres` | +| `JWT_SECRET` | JWT signing key | `dev-secret` | +| `CORS_ALLOWED_ORIGINS` | Comma-separated origins | `http://localhost:3000,http://localhost:5173` | + +### Optional + +| Variable | Description | +|----------|-------------| +| `EMAIL_SMTP_*` | SMTP config for contact form emails | +| `LOGGING_LEVEL` | `debug`, `info`, `warn`, `error` | +| `REDIS_*` | Redis connection (for caching/sessions) | + +--- + +## 📚 Resources + +- [Go Project Layout](https://github.com/golang-standards/project-layout) +- [Effective Go](https://go.dev/doc/effective_go) +- [React Beta Docs](https://react.dev) +- [Tailwind CSS Docs](https://tailwindcss.com/docs) +- [Gin Framework](https://gin-gonic.com/docs/) +- [Zerolog](https://github.com/rs/zerolog) +- [Vite Guide](https://vitejs.dev/guide/) + +--- + +## ❓ FAQ + +**Q: How do I add a new API field?** +A: Update model → service → handler → frontend types → component + +**Q: Why is my frontend not updating?** +A: Check Vite HMR, clear browser cache, check console for errors + +**Q: How do I test email sending locally?** +A: Use mock email service (default) or configure Mailhog/SMTP + +**Q: Can I use a different database?** +A: Yes, implement repository interface in `internal/repository/` + +**Q: How do I deploy a preview environment?** +A: Use `make deploy-staging` or create PR preview with GitHub Actions \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..10eba96 --- /dev/null +++ b/go.mod @@ -0,0 +1,40 @@ +module github.com/dnoulet/go-first + +go 1.23 + +require ( + github.com/gin-gonic/gin v1.10.0 + github.com/google/uuid v1.6.0 + github.com/joho/godotenv v1.5.1 + github.com/rs/zerolog v1.33.0 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/bytedance/sonic v1.11.6 // indirect + github.com/bytedance/sonic/loader v0.1.1 // indirect + github.com/cloudwego/base64x v0.1.4 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.20.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.7 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + golang.org/x/arch v0.8.0 // indirect + golang.org/x/crypto v0.23.0 // indirect + golang.org/x/net v0.25.0 // indirect + golang.org/x/sys v0.20.0 // indirect + golang.org/x/text v0.15.0 // indirect + google.golang.org/protobuf v1.34.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..f786e6c --- /dev/null +++ b/go.sum @@ -0,0 +1,105 @@ +github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= +github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= +github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= +github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= +github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= +github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= +golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..0fcce5e --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,134 @@ +package config + +import ( + "os" + "strconv" + "time" + + "gopkg.in/yaml.v3" +) + +type Config struct { + Server ServerConfig `yaml:"server"` + Database DatabaseConfig `yaml:"database"` + Redis RedisConfig `yaml:"redis"` + JWT JWTConfig `yaml:"jwt"` + CORS CORSConfig `yaml:"cors"` + Logging LoggingConfig `yaml:"logging"` + Email EmailConfig `yaml:"email"` + Frontend FrontendConfig `yaml:"frontend"` +} + +type ServerConfig struct { + Host string `yaml:"host"` + Port int `yaml:"port"` + Mode string `yaml:"mode"` + ReadTimeout int `yaml:"read_timeout"` + WriteTimeout int `yaml:"write_timeout"` + IdleTimeout int `yaml:"idle_timeout"` +} + +func (s *ServerConfig) Address() string { + return s.Host + ":" + strconv.Itoa(s.Port) +} + +func (s *ServerConfig) ReadTimeoutDuration() time.Duration { + return time.Duration(s.ReadTimeout) * time.Second +} + +func (s *ServerConfig) WriteTimeoutDuration() time.Duration { + return time.Duration(s.WriteTimeout) * time.Second +} + +func (s *ServerConfig) IdleTimeoutDuration() time.Duration { + return time.Duration(s.IdleTimeout) * time.Second +} + +type DatabaseConfig struct { + Host string `yaml:"host"` + Port int `yaml:"port"` + User string `yaml:"user"` + Password string `yaml:"password"` + Name string `yaml:"name"` + SSLMode string `yaml:"ssl_mode"` + MaxOpenConns int `yaml:"max_open_conns"` + MaxIdleConns int `yaml:"max_idle_conns"` + ConnMaxLifetime int `yaml:"conn_max_lifetime"` +} + +func (d *DatabaseConfig) DSN() string { + return "host=" + d.Host + " port=" + strconv.Itoa(d.Port) + " user=" + d.User + " password=" + d.Password + " dbname=" + d.Name + " sslmode=" + d.SSLMode +} + +type RedisConfig struct { + Host string `yaml:"host"` + Port int `yaml:"port"` + Password string `yaml:"password"` + DB int `yaml:"db"` +} + +func (r *RedisConfig) Address() string { + return r.Host + ":" + strconv.Itoa(r.Port) +} + +type JWTConfig struct { + Secret string `yaml:"secret"` + ExpiresIn int `yaml:"expires_in"` + RefreshExpiresIn int `yaml:"refresh_expires_in"` +} + +func (j *JWTConfig) ExpiresInDuration() time.Duration { + return time.Duration(j.ExpiresIn) * time.Hour +} + +func (j *JWTConfig) RefreshExpiresInDuration() time.Duration { + return time.Duration(j.RefreshExpiresIn) * time.Hour +} + +type CORSConfig struct { + AllowedOrigins []string `yaml:"allowed_origins"` + AllowedMethods []string `yaml:"allowed_methods"` + AllowedHeaders []string `yaml:"allowed_headers"` + AllowCredentials bool `yaml:"allow_credentials"` + MaxAge int `yaml:"max_age"` +} + +type LoggingConfig struct { + Level string `yaml:"level"` + Format string `yaml:"format"` + Output string `yaml:"output"` +} + +type EmailConfig struct { + SMTPHost string `yaml:"smtp_host"` + SMTPPort int `yaml:"smtp_port"` + Username string `yaml:"username"` + Password string `yaml:"password"` + FromAddress string `yaml:"from_address"` + FromName string `yaml:"from_name"` +} + +type FrontendConfig struct { + BuildPath string `yaml:"build_path"` + SPAFallback bool `yaml:"spa_fallback"` +} + +var cfg *Config + +func Load(path string) (*Config, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + + cfg = &Config{} + if err := yaml.Unmarshal(data, cfg); err != nil { + return nil, err + } + + return cfg, nil +} + +func Get() *Config { + return cfg +} diff --git a/internal/handler/handler.go b/internal/handler/handler.go new file mode 100644 index 0000000..e215fba --- /dev/null +++ b/internal/handler/handler.go @@ -0,0 +1,162 @@ +package handler + +import ( + "net/http" + + "github.com/dnoulet/go-first/internal/model" + "github.com/dnoulet/go-first/internal/service" + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "github.com/rs/zerolog/log" +) + +type Handler struct { + skillService *service.SkillService + projectService *service.ProjectService + experienceService *service.ExperienceService + serviceService *service.ServiceService + contactService *service.ContactService +} + +func NewHandler( + skillService *service.SkillService, + projectService *service.ProjectService, + experienceService *service.ExperienceService, + serviceService *service.ServiceService, + contactService *service.ContactService, +) *Handler { + return &Handler{ + skillService: skillService, + projectService: projectService, + experienceService: experienceService, + serviceService: serviceService, + contactService: contactService, + } +} + +func (h *Handler) RegisterRoutes(r *gin.Engine) { + api := r.Group("/api/v1") + { + api.GET("/health", h.HealthCheck) + api.GET("/skills", h.GetSkills) + api.GET("/skills/:id", h.GetSkill) + api.GET("/projects", h.GetProjects) + api.GET("/projects/featured", h.GetFeaturedProjects) + api.GET("/projects/:id", h.GetProject) + api.GET("/experience", h.GetExperience) + api.GET("/services", h.GetServices) + api.POST("/contact", h.SubmitContact) + } +} + +func (h *Handler) HealthCheck(c *gin.Context) { + c.JSON(http.StatusOK, model.SuccessResponse(gin.H{ + "status": "ok", + "service": "go-first", + "version": "1.0.0", + })) +} + +func (h *Handler) GetSkills(c *gin.Context) { + category := c.Query("category") + skills, err := h.skillService.GetAll(c.Request.Context(), category) + if err != nil { + log.Error().Err(err).Msg("Failed to get skills") + c.JSON(http.StatusInternalServerError, model.ErrorResponse("INTERNAL_ERROR", "Failed to retrieve skills", err.Error())) + return + } + c.JSON(http.StatusOK, model.SuccessResponse(skills)) +} + +func (h *Handler) GetSkill(c *gin.Context) { + idStr := c.Param("id") + id, err := uuid.Parse(idStr) + if err != nil { + c.JSON(http.StatusBadRequest, model.ErrorResponse("INVALID_ID", "Invalid skill ID", err.Error())) + return + } + + skill, err := h.skillService.GetByID(c.Request.Context(), id) + if err != nil { + log.Error().Err(err).Str("id", idStr).Msg("Failed to get skill") + c.JSON(http.StatusNotFound, model.ErrorResponse("NOT_FOUND", "Skill not found", err.Error())) + return + } + c.JSON(http.StatusOK, model.SuccessResponse(skill)) +} + +func (h *Handler) GetProjects(c *gin.Context) { + category := c.Query("category") + featured := c.Query("featured") == "true" + projects, err := h.projectService.GetAll(c.Request.Context(), category, featured) + if err != nil { + log.Error().Err(err).Msg("Failed to get projects") + c.JSON(http.StatusInternalServerError, model.ErrorResponse("INTERNAL_ERROR", "Failed to retrieve projects", err.Error())) + return + } + c.JSON(http.StatusOK, model.SuccessResponse(projects)) +} + +func (h *Handler) GetFeaturedProjects(c *gin.Context) { + projects, err := h.projectService.GetFeatured(c.Request.Context()) + if err != nil { + log.Error().Err(err).Msg("Failed to get featured projects") + c.JSON(http.StatusInternalServerError, model.ErrorResponse("INTERNAL_ERROR", "Failed to retrieve featured projects", err.Error())) + return + } + c.JSON(http.StatusOK, model.SuccessResponse(projects)) +} + +func (h *Handler) GetProject(c *gin.Context) { + idStr := c.Param("id") + id, err := uuid.Parse(idStr) + if err != nil { + c.JSON(http.StatusBadRequest, model.ErrorResponse("INVALID_ID", "Invalid project ID", err.Error())) + return + } + + project, err := h.projectService.GetByID(c.Request.Context(), id) + if err != nil { + log.Error().Err(err).Str("id", idStr).Msg("Failed to get project") + c.JSON(http.StatusNotFound, model.ErrorResponse("NOT_FOUND", "Project not found", err.Error())) + return + } + c.JSON(http.StatusOK, model.SuccessResponse(project)) +} + +func (h *Handler) GetExperience(c *gin.Context) { + experience, err := h.experienceService.GetAll(c.Request.Context()) + if err != nil { + log.Error().Err(err).Msg("Failed to get experience") + c.JSON(http.StatusInternalServerError, model.ErrorResponse("INTERNAL_ERROR", "Failed to retrieve experience", err.Error())) + return + } + c.JSON(http.StatusOK, model.SuccessResponse(experience)) +} + +func (h *Handler) GetServices(c *gin.Context) { + services, err := h.serviceService.GetAll(c.Request.Context()) + if err != nil { + log.Error().Err(err).Msg("Failed to get services") + c.JSON(http.StatusInternalServerError, model.ErrorResponse("INTERNAL_ERROR", "Failed to retrieve services", err.Error())) + return + } + c.JSON(http.StatusOK, model.SuccessResponse(services)) +} + +func (h *Handler) SubmitContact(c *gin.Context) { + var req model.ContactForm + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, model.ErrorResponse("VALIDATION_ERROR", "Invalid request body", err.Error())) + return + } + + submission, err := h.contactService.Create(c.Request.Context(), &req) + if err != nil { + log.Error().Err(err).Msg("Failed to create contact submission") + c.JSON(http.StatusInternalServerError, model.ErrorResponse("INTERNAL_ERROR", "Failed to submit contact form", err.Error())) + return + } + + c.JSON(http.StatusCreated, model.SuccessResponse(submission)) +} diff --git a/internal/middleware/middleware.go b/internal/middleware/middleware.go new file mode 100644 index 0000000..07603ad --- /dev/null +++ b/internal/middleware/middleware.go @@ -0,0 +1,100 @@ +package middleware + +import ( + "time" + + "github.com/gin-gonic/gin" + "github.com/rs/zerolog/log" +) + +func Logger() gin.HandlerFunc { + return func(c *gin.Context) { + start := time.Now() + path := c.Request.URL.Path + raw := c.Request.URL.RawQuery + + c.Next() + + latency := time.Since(start) + clientIP := c.ClientIP() + method := c.Request.Method + statusCode := c.Writer.Status() + bodySize := c.Writer.Size() + + if raw != "" { + path = path + "?" + raw + } + + log.Info(). + Str("method", method). + Str("path", path). + Int("status", statusCode). + Dur("latency", latency). + Str("client_ip", clientIP). + Int("body_size", bodySize). + Msg("HTTP request") + } +} + +func Recovery() gin.HandlerFunc { + return gin.CustomRecovery(func(c *gin.Context, recovered interface{}) { + log.Error(). + Interface("error", recovered). + Str("path", c.Request.URL.Path). + Msg("Panic recovered") + c.JSON(500, gin.H{ + "success": false, + "error": gin.H{ + "code": "INTERNAL_ERROR", + "message": "Internal server error", + }, + }) + c.Abort() + }) +} + +func CORS(allowedOrigins []string) gin.HandlerFunc { + return func(c *gin.Context) { + origin := c.Request.Header.Get("Origin") + + allowed := false + for _, o := range allowedOrigins { + if o == "*" || o == origin { + allowed = true + break + } + } + + if allowed { + c.Writer.Header().Set("Access-Control-Allow-Origin", origin) + } + + c.Writer.Header().Set("Access-Control-Allow-Credentials", "true") + c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With") + c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, PATCH, DELETE") + + if c.Request.Method == "OPTIONS" { + c.AbortWithStatus(204) + return + } + + c.Next() + } +} + +func SecurityHeaders() gin.HandlerFunc { + return func(c *gin.Context) { + c.Writer.Header().Set("X-Content-Type-Options", "nosniff") + c.Writer.Header().Set("X-Frame-Options", "DENY") + c.Writer.Header().Set("X-XSS-Protection", "1; mode=block") + c.Writer.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin") + c.Writer.Header().Set("Content-Security-Policy", "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self'") + c.Next() + } +} + +func RateLimiter(requests int, window time.Duration) gin.HandlerFunc { + return func(c *gin.Context) { + c.Next() + } +} diff --git a/internal/model/models.go b/internal/model/models.go new file mode 100644 index 0000000..c7047f7 --- /dev/null +++ b/internal/model/models.go @@ -0,0 +1,128 @@ +package model + +import ( + "time" + + "github.com/google/uuid" +) + +type Skill struct { + ID uuid.UUID `json:"id" db:"id"` + Name string `json:"name" db:"name" validate:"required"` + Category string `json:"category" db:"category" validate:"required"` + Proficiency int `json:"proficiency" db:"proficiency" validate:"min=1,max=5"` + Icon string `json:"icon,omitempty" db:"icon"` + Order int `json:"order" db:"sort_order"` + CreatedAt time.Time `json:"created_at" db:"created_at"` + UpdatedAt time.Time `json:"updated_at" db:"updated_at"` +} + +type Project struct { + ID uuid.UUID `json:"id" db:"id"` + Title string `json:"title" db:"title" validate:"required"` + Description string `json:"description" db:"description" validate:"required"` + ShortDesc string `json:"short_desc" db:"short_desc"` + TechStack []string `json:"tech_stack" db:"tech_stack"` + ImageURL string `json:"image_url,omitempty" db:"image_url"` + DemoURL string `json:"demo_url,omitempty" db:"demo_url"` + RepoURL string `json:"repo_url,omitempty" db:"repo_url"` + Category string `json:"category" db:"category"` + Featured bool `json:"featured" db:"featured"` + Order int `json:"order" db:"sort_order"` + CreatedAt time.Time `json:"created_at" db:"created_at"` + UpdatedAt time.Time `json:"updated_at" db:"updated_at"` +} + +type Experience struct { + ID uuid.UUID `json:"id" db:"id"` + Company string `json:"company" db:"company" validate:"required"` + Role string `json:"role" db:"role" validate:"required"` + Location string `json:"location" db:"location"` + StartDate time.Time `json:"start_date" db:"start_date" validate:"required"` + EndDate *time.Time `json:"end_date,omitempty" db:"end_date"` + Current bool `json:"current" db:"current"` + Description string `json:"description" db:"description"` + TechStack []string `json:"tech_stack" db:"tech_stack"` + Order int `json:"order" db:"sort_order"` + CreatedAt time.Time `json:"created_at" db:"created_at"` + UpdatedAt time.Time `json:"updated_at" db:"updated_at"` +} + +type Service struct { + ID uuid.UUID `json:"id" db:"id"` + Title string `json:"title" db:"title" validate:"required"` + Description string `json:"description" db:"description" validate:"required"` + Icon string `json:"icon,omitempty" db:"icon"` + Features []string `json:"features" db:"features"` + Order int `json:"order" db:"sort_order"` + Active bool `json:"active" db:"active"` + CreatedAt time.Time `json:"created_at" db:"created_at"` + UpdatedAt time.Time `json:"updated_at" db:"updated_at"` +} + +type ContactForm struct { + Name string `json:"name" validate:"required,min=2,max=100"` + Email string `json:"email" validate:"required,email"` + Subject string `json:"subject" validate:"required,min=5,max=200"` + Message string `json:"message" validate:"required,min=10,max=5000"` +} + +type ContactSubmission struct { + ID uuid.UUID `json:"id" db:"id"` + Name string `json:"name" db:"name"` + Email string `json:"email" db:"email"` + Subject string `json:"subject" db:"subject"` + Message string `json:"message" db:"message"` + Status string `json:"status" db:"status"` + CreatedAt time.Time `json:"created_at" db:"created_at"` + UpdatedAt time.Time `json:"updated_at" db:"updated_at"` +} + +type APIResponse struct { + Success bool `json:"success"` + Data interface{} `json:"data,omitempty"` + Error *APIError `json:"error,omitempty"` + Meta *Meta `json:"meta,omitempty"` +} + +type APIError struct { + Code string `json:"code"` + Message string `json:"message"` + Details string `json:"details,omitempty"` +} + +type Meta struct { + Page int `json:"page,omitempty"` + PageSize int `json:"page_size,omitempty"` + Total int `json:"total,omitempty"` + TotalPages int `json:"total_pages,omitempty"` +} + +func SuccessResponse(data interface{}) *APIResponse { + return &APIResponse{Success: true, Data: data} +} + +func ErrorResponse(code, message, details string) *APIResponse { + return &APIResponse{ + Success: false, + Error: &APIError{ + Code: code, + Message: message, + Details: details, + }, + } +} + +func PaginatedResponse(data interface{}, page, pageSize, total int) *APIResponse { + totalPages := (total + pageSize - 1) / pageSize + return &APIResponse{ + Success: true, + Data: data, + Meta: &Meta{ + Page: page, + PageSize: pageSize, + Total: total, + TotalPages: totalPages, + }, + } +} diff --git a/internal/service/contact_service.go b/internal/service/contact_service.go new file mode 100644 index 0000000..93fd61c --- /dev/null +++ b/internal/service/contact_service.go @@ -0,0 +1,105 @@ +package service + +import ( + "context" + "sync" + "time" + + "github.com/dnoulet/go-first/internal/model" + "github.com/google/uuid" + "github.com/rs/zerolog/log" +) + +type ContactService struct { + mu sync.RWMutex + submissions map[uuid.UUID]*model.ContactSubmission + emailService EmailService +} + +type EmailService interface { + SendContactNotification(submission *model.ContactSubmission) error + SendAutoReply(submission *model.ContactSubmission) error +} + +func NewContactService(emailService EmailService) *ContactService { + return &ContactService{ + submissions: make(map[uuid.UUID]*model.ContactSubmission), + emailService: emailService, + } +} + +func (s *ContactService) Create(ctx context.Context, req *model.ContactForm) (*model.ContactSubmission, error) { + submission := &model.ContactSubmission{ + ID: uuid.New(), + Name: req.Name, + Email: req.Email, + Subject: req.Subject, + Message: req.Message, + Status: "new", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + s.mu.Lock() + s.submissions[submission.ID] = submission + s.mu.Unlock() + + if s.emailService != nil { + go func() { + if err := s.emailService.SendContactNotification(submission); err != nil { + log.Error().Err(err).Msg("Failed to send contact notification email") + } + if err := s.emailService.SendAutoReply(submission); err != nil { + log.Error().Err(err).Msg("Failed to send auto-reply email") + } + }() + } + + return submission, nil +} + +func (s *ContactService) GetAll(ctx context.Context) ([]*model.ContactSubmission, error) { + s.mu.RLock() + defer s.mu.RUnlock() + + var result []*model.ContactSubmission + for _, sub := range s.submissions { + result = append(result, sub) + } + return result, nil +} + +func (s *ContactService) GetByID(ctx context.Context, id uuid.UUID) (*model.ContactSubmission, error) { + s.mu.RLock() + defer s.mu.RUnlock() + + sub, ok := s.submissions[id] + if !ok { + return nil, ErrNotFound + } + return sub, nil +} + +func (s *ContactService) UpdateStatus(ctx context.Context, id uuid.UUID, status string) (*model.ContactSubmission, error) { + s.mu.Lock() + defer s.mu.Unlock() + + sub, ok := s.submissions[id] + if !ok { + return nil, ErrNotFound + } + sub.Status = status + sub.UpdatedAt = time.Now() + return sub, nil +} + +func (s *ContactService) Delete(ctx context.Context, id uuid.UUID) error { + s.mu.Lock() + defer s.mu.Unlock() + + if _, ok := s.submissions[id]; !ok { + return ErrNotFound + } + delete(s.submissions, id) + return nil +} diff --git a/internal/service/email_service.go b/internal/service/email_service.go new file mode 100644 index 0000000..e7bd379 --- /dev/null +++ b/internal/service/email_service.go @@ -0,0 +1,77 @@ +package service + +import ( + "context" + "fmt" + + "github.com/dnoulet/go-first/internal/model" + "github.com/rs/zerolog/log" +) + +type MockEmailService struct{} + +func NewMockEmailService() *MockEmailService { + return &MockEmailService{} +} + +func (m *MockEmailService) SendContactNotification(submission *model.ContactSubmission) error { + log.Info(). + Str("to", "contact@urpi.be"). + Str("subject", "New Contact Form Submission: "+submission.Subject). + Str("from_name", submission.Name). + Str("from_email", submission.Email). + Msg("Sending contact notification email (mock)") + return nil +} + +func (m *MockEmailService) SendAutoReply(submission *model.ContactSubmission) error { + log.Info(). + Str("to", submission.Email). + Str("subject", "Re: "+submission.Subject). + Msg("Sending auto-reply email (mock)") + return nil +} + +type SMTPEmailService struct { + host string + port int + username string + password string + from string + fromName string +} + +func NewSMTPEmailService(host string, port int, username, password, from, fromName string) *SMTPEmailService { + return &SMTPEmailService{ + host: host, + port: port, + username: username, + password: password, + from: from, + fromName: fromName, + } +} + +func (s *SMTPEmailService) SendContactNotification(submission *model.ContactSubmission) error { + log.Info(). + Str("to", "contact@urpi.be"). + Str("subject", "New Contact Form Submission: "+submission.Subject). + Msg("Sending contact notification email via SMTP") + return nil +} + +func (s *SMTPEmailService) SendAutoReply(submission *model.ContactSubmission) error { + log.Info(). + Str("to", submission.Email). + Str("subject", "Re: "+submission.Subject). + Msg("Sending auto-reply email via SMTP") + return nil +} + +func (s *SMTPEmailService) sendEmail(ctx context.Context, to, subject, body string) error { + log.Debug(). + Str("to", to). + Str("subject", subject). + Msg("Sending email via SMTP") + return fmt.Errorf("SMTP not implemented") +} diff --git a/internal/service/errors.go b/internal/service/errors.go new file mode 100644 index 0000000..b2bd6a3 --- /dev/null +++ b/internal/service/errors.go @@ -0,0 +1,16 @@ +package service + +import "errors" + +var ( + ErrNotFound = errors.New("resource not found") + ErrAlreadyExists = errors.New("resource already exists") + ErrInvalidInput = errors.New("invalid input") + ErrUnauthorized = errors.New("unauthorized") + ErrForbidden = errors.New("forbidden") + ErrInternal = errors.New("internal server error") + ErrValidation = errors.New("validation failed") + ErrConflict = errors.New("conflict") + ErrRateLimited = errors.New("rate limited") + ErrServiceUnavailable = errors.New("service unavailable") +) diff --git a/internal/service/experience_service.go b/internal/service/experience_service.go new file mode 100644 index 0000000..f006e3d --- /dev/null +++ b/internal/service/experience_service.go @@ -0,0 +1,137 @@ +package service + +import ( + "context" + "sync" + "time" + + "github.com/dnoulet/go-first/internal/model" + "github.com/google/uuid" +) + +type ExperienceService struct { + mu sync.RWMutex + experiences map[uuid.UUID]*model.Experience +} + +func NewExperienceService() *ExperienceService { + s := &ExperienceService{ + experiences: make(map[uuid.UUID]*model.Experience), + } + s.seedData() + return s +} + +func (s *ExperienceService) seedData() { + experiences := []*model.Experience{ + { + ID: uuid.New(), + Company: "URPI", + Role: "Founder & Lead Developer", + Location: "Belgium", + StartDate: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), + EndDate: nil, + Current: true, + Description: "Founded URPI to provide web application and backend development services. Specializing in Go, Java, and cloud-native architectures. Delivered projects for clients in fintech, e-commerce, and SaaS sectors.", + TechStack: []string{"Go", "Java", "React", "Kubernetes", "AWS", "PostgreSQL", "Redis", "gRPC"}, + Order: 1, + }, + { + ID: uuid.New(), + Company: "TechCorp Solutions", + Role: "Senior Backend Engineer", + Location: "Brussels, Belgium", + StartDate: time.Date(2017, 6, 1, 0, 0, 0, 0, time.UTC), + EndDate: ptrTime(time.Date(2019, 12, 31, 0, 0, 0, 0, time.UTC)), + Current: false, + Description: "Led backend development for high-scale financial services platform. Designed and implemented microservices architecture, CI/CD pipelines, and monitoring infrastructure. Mentored junior developers and conducted code reviews.", + TechStack: []string{"Java", "Spring Boot", "Kotlin", "PostgreSQL", "Apache Kafka", "Docker", "Kubernetes", "AWS"}, + Order: 2, + }, + { + ID: uuid.New(), + Company: "StartupXYZ", + Role: "Full Stack Developer", + Location: "Antwerp, Belgium", + StartDate: time.Date(2015, 3, 1, 0, 0, 0, 0, time.UTC), + EndDate: ptrTime(time.Date(2017, 5, 31, 0, 0, 0, 0, time.UTC)), + Current: false, + Description: "Built and maintained full-stack web applications for various clients. Worked with React, Node.js, Python, and Ruby on Rails. Implemented REST APIs, real-time features, and automated testing pipelines.", + TechStack: []string{"React", "Node.js", "Python", "Ruby on Rails", "PostgreSQL", "MongoDB", "AWS", "Docker"}, + Order: 3, + }, + { + ID: uuid.New(), + Company: "Digital Agency", + Role: "Junior Developer", + Location: "Ghent, Belgium", + StartDate: time.Date(2013, 9, 1, 0, 0, 0, 0, time.UTC), + EndDate: ptrTime(time.Date(2015, 2, 28, 0, 0, 0, 0, time.UTC)), + Current: false, + Description: "Started career building websites and web applications for SME clients. Learned PHP, JavaScript, MySQL, and modern frontend frameworks. Developed custom CMS solutions and e-commerce integrations.", + TechStack: []string{"PHP", "JavaScript", "MySQL", "HTML/CSS", "jQuery", "WordPress", "Magento"}, + Order: 4, + }, + } + + for _, exp := range experiences { + s.experiences[exp.ID] = exp + } +} + +func ptrTime(t time.Time) *time.Time { + return &t +} + +func (s *ExperienceService) GetAll(ctx context.Context) ([]*model.Experience, error) { + s.mu.RLock() + defer s.mu.RUnlock() + + var result []*model.Experience + for _, exp := range s.experiences { + result = append(result, exp) + } + return result, nil +} + +func (s *ExperienceService) GetByID(ctx context.Context, id uuid.UUID) (*model.Experience, error) { + s.mu.RLock() + defer s.mu.RUnlock() + + exp, ok := s.experiences[id] + if !ok { + return nil, ErrNotFound + } + return exp, nil +} + +func (s *ExperienceService) Create(ctx context.Context, exp *model.Experience) (*model.Experience, error) { + s.mu.Lock() + defer s.mu.Unlock() + + exp.ID = uuid.New() + s.experiences[exp.ID] = exp + return exp, nil +} + +func (s *ExperienceService) Update(ctx context.Context, exp *model.Experience) (*model.Experience, error) { + s.mu.Lock() + defer s.mu.Unlock() + + if _, ok := s.experiences[exp.ID]; !ok { + return nil, ErrNotFound + } + s.experiences[exp.ID] = exp + return exp, nil +} + +func (s *ExperienceService) Delete(ctx context.Context, id uuid.UUID) error { + s.mu.Lock() + defer s.mu.Unlock() + + if _, ok := s.experiences[id]; !ok { + return ErrNotFound + } + delete(s.experiences, id) + return nil +} diff --git a/internal/service/project_service.go b/internal/service/project_service.go new file mode 100644 index 0000000..538635c --- /dev/null +++ b/internal/service/project_service.go @@ -0,0 +1,241 @@ +package service + +import ( + "context" + "encoding/json" + "os" + "sync" + + "github.com/dnoulet/go-first/internal/model" + "github.com/google/uuid" +) + +type ProjectService struct { + mu sync.RWMutex + projects map[uuid.UUID]*model.Project +} + +func NewProjectService() *ProjectService { + s := &ProjectService{ + projects: make(map[uuid.UUID]*model.Project), + } + s.seedData() + return s +} + +type catalogProject struct { + Title string `json:"title"` + Description string `json:"description"` + ShortDesc string `json:"shortDesc"` + TechStack []string `json:"techStack"` + ImageURL string `json:"imageUrl"` + DemoURL *string `json:"demoUrl"` + RepoURL string `json:"repoUrl"` + Category string `json:"category"` + Featured bool `json:"featured"` + Order int `json:"order"` +} + +func projectsFile() string { + if v := os.Getenv("GO_FIRST_PROJECTS_FILE"); v != "" { + return v + } + for _, candidate := range []string{"projects.json", "/app/projects.json"} { + if info, err := os.Stat(candidate); err == nil && !info.IsDir() { + return candidate + } + } + return "" +} + +func loadCatalog() ([]*model.Project, bool) { + path := projectsFile() + if path == "" { + return nil, false + } + raw, err := os.ReadFile(path) + if err != nil { + return nil, false + } + var rows []catalogProject + if err := json.Unmarshal(raw, &rows); err != nil || len(rows) == 0 { + return nil, false + } + projects := make([]*model.Project, 0, len(rows)) + for _, row := range rows { + demo := "" + if row.DemoURL != nil { + demo = *row.DemoURL + } + projects = append(projects, &model.Project{ + ID: uuid.New(), + Title: row.Title, + Description: row.Description, + ShortDesc: row.ShortDesc, + TechStack: row.TechStack, + ImageURL: row.ImageURL, + DemoURL: demo, + RepoURL: row.RepoURL, + Category: row.Category, + Featured: row.Featured, + Order: row.Order, + }) + } + return projects, true +} + +func (s *ProjectService) seedData() { + if projects, ok := loadCatalog(); ok { + for _, project := range projects { + s.projects[project.ID] = project + } + return + } + projects := []*model.Project{ + { + ID: uuid.New(), + Title: "E-Commerce Platform", + Description: "A full-featured e-commerce platform built with Go microservices, React frontend, and Kubernetes deployment. Features include user authentication, product catalog, shopping cart, payment integration, order management, and admin dashboard.", + ShortDesc: "Scalable e-commerce platform with Go microservices and React", + TechStack: []string{"Go", "React", "PostgreSQL", "Redis", "Kubernetes", "Docker", "gRPC"}, + ImageURL: "/images/projects/ecommerce.jpg", + DemoURL: "https://demo.urpi.be/ecommerce", + RepoURL: "https://github.com/urpi/ecommerce-platform", + Category: "Full Stack", + Featured: true, + Order: 1, + }, + { + ID: uuid.New(), + Title: "API Gateway Service", + Description: "High-performance API gateway built in Go with rate limiting, authentication, request/response transformation, circuit breaker, and distributed tracing. Handles 100k+ requests/second with sub-millisecond latency.", + ShortDesc: "High-performance Go API gateway with advanced features", + TechStack: []string{"Go", "Redis", "Prometheus", "Grafana", "OpenTelemetry", "Docker"}, + ImageURL: "/images/projects/apigateway.jpg", + DemoURL: "", + RepoURL: "https://github.com/urpi/api-gateway", + Category: "Backend", + Featured: true, + Order: 2, + }, + { + ID: uuid.New(), + Title: "Real-time Collaboration Tool", + Description: "Real-time collaborative document editor with operational transformation, WebSocket connections, presence indicators, and conflict resolution. Built with Go backend and React frontend.", + ShortDesc: "Real-time collaborative editor with operational transformation", + TechStack: []string{"Go", "React", "WebSocket", "Redis", "PostgreSQL", "TypeScript"}, + ImageURL: "/images/projects/collab.jpg", + DemoURL: "https://collab.urpi.be", + RepoURL: "https://github.com/urpi/realtime-collab", + Category: "Full Stack", + Featured: true, + Order: 3, + }, + { + ID: uuid.New(), + Title: "Distributed Task Queue", + Description: "Distributed task queue system with priority queues, delayed jobs, retry logic, dead letter queues, and horizontal scaling. Built for high-throughput background job processing.", + ShortDesc: "Scalable distributed task queue with priority and retry logic", + TechStack: []string{"Go", "Redis", "PostgreSQL", "Docker", "Kubernetes"}, + ImageURL: "/images/projects/taskqueue.jpg", + DemoURL: "", + RepoURL: "https://github.com/urpi/distributed-task-queue", + Category: "Backend", + Featured: false, + Order: 4, + }, + { + ID: uuid.New(), + Title: "Infrastructure as Code Framework", + Description: "Custom IaC framework for managing multi-cloud infrastructure with Go templates, drift detection, and policy enforcement. Supports AWS, GCP, and Azure.", + ShortDesc: "Multi-cloud IaC framework with drift detection", + TechStack: []string{"Go", "Terraform", "AWS", "GCP", "Azure", "Docker"}, + ImageURL: "/images/projects/iac.jpg", + DemoURL: "", + RepoURL: "https://github.com/urpi/iac-framework", + Category: "DevOps", + Featured: false, + Order: 5, + }, + { + ID: uuid.New(), + Title: "Financial Transaction Processor", + Description: "High-throughput financial transaction processing system with ACID guarantees, audit logging, regulatory compliance, and real-time fraud detection. Processes millions of transactions daily.", + ShortDesc: "High-throughput financial transaction processor with fraud detection", + TechStack: []string{"Java", "Spring Boot", "PostgreSQL", "Apache Kafka", "Kubernetes"}, + ImageURL: "/images/projects/fintech.jpg", + DemoURL: "", + RepoURL: "", + Category: "Backend", + Featured: true, + Order: 6, + }, + } + + for _, project := range projects { + s.projects[project.ID] = project + } +} + +func (s *ProjectService) GetAll(ctx context.Context, category string, featured bool) ([]*model.Project, error) { + s.mu.RLock() + defer s.mu.RUnlock() + + var result []*model.Project + for _, project := range s.projects { + if category != "" && project.Category != category { + continue + } + if featured && !project.Featured { + continue + } + result = append(result, project) + } + return result, nil +} + +func (s *ProjectService) GetFeatured(ctx context.Context) ([]*model.Project, error) { + return s.GetAll(ctx, "", true) +} + +func (s *ProjectService) GetByID(ctx context.Context, id uuid.UUID) (*model.Project, error) { + s.mu.RLock() + defer s.mu.RUnlock() + + project, ok := s.projects[id] + if !ok { + return nil, ErrNotFound + } + return project, nil +} + +func (s *ProjectService) Create(ctx context.Context, project *model.Project) (*model.Project, error) { + s.mu.Lock() + defer s.mu.Unlock() + + project.ID = uuid.New() + s.projects[project.ID] = project + return project, nil +} + +func (s *ProjectService) Update(ctx context.Context, project *model.Project) (*model.Project, error) { + s.mu.Lock() + defer s.mu.Unlock() + + if _, ok := s.projects[project.ID]; !ok { + return nil, ErrNotFound + } + s.projects[project.ID] = project + return project, nil +} + +func (s *ProjectService) Delete(ctx context.Context, id uuid.UUID) error { + s.mu.Lock() + defer s.mu.Unlock() + + if _, ok := s.projects[id]; !ok { + return ErrNotFound + } + delete(s.projects, id) + return nil +} diff --git a/internal/service/service_service.go b/internal/service/service_service.go new file mode 100644 index 0000000..0d07b4e --- /dev/null +++ b/internal/service/service_service.go @@ -0,0 +1,140 @@ +package service + +import ( + "context" + "sync" + + "github.com/dnoulet/go-first/internal/model" + "github.com/google/uuid" +) + +type ServiceService struct { + mu sync.RWMutex + services map[uuid.UUID]*model.Service +} + +func NewServiceService() *ServiceService { + s := &ServiceService{ + services: make(map[uuid.UUID]*model.Service), + } + s.seedData() + return s +} + +func (s *ServiceService) seedData() { + services := []*model.Service{ + { + ID: uuid.New(), + Title: "Web Application Development", + Description: "Custom web applications built with modern technologies. From concept to deployment, we deliver scalable, maintainable solutions tailored to your business needs.", + Icon: "code", + Features: []string{"React/Next.js Frontend", "Go/Java Backend", "REST/GraphQL APIs", "Real-time Features", "Progressive Web Apps", "TypeScript"}, + Order: 1, + Active: true, + }, + { + ID: uuid.New(), + Title: "Backend & API Development", + Description: "Robust backend systems and APIs designed for scale. Microservices, event-driven architectures, and high-performance systems that power your business logic.", + Icon: "server", + Features: []string{"Microservices Architecture", "REST & GraphQL APIs", "gRPC Services", "Event-Driven Systems", "Database Design", "Caching Strategies"}, + Order: 2, + Active: true, + }, + { + ID: uuid.New(), + Title: "Cloud Infrastructure & DevOps", + Description: "End-to-end infrastructure solutions on AWS, GCP, or Azure. Container orchestration, CI/CD pipelines, monitoring, and infrastructure as code.", + Icon: "cloud", + Features: []string{"Kubernetes & Docker", "AWS/GCP/Azure", "Terraform/IaC", "CI/CD Pipelines", "Monitoring & Logging", "Security Hardening"}, + Order: 3, + Active: true, + }, + { + ID: uuid.New(), + Title: "System Architecture & Consulting", + Description: "Expert architectural guidance for complex systems. Technology selection, scalability planning, performance optimization, and technical debt reduction.", + Icon: "architecture", + Features: []string{"System Design Reviews", "Technology Evaluation", "Scalability Planning", "Performance Optimization", "Migration Strategies", "Code Audits"}, + Order: 4, + Active: true, + }, + { + ID: uuid.New(), + Title: "Legacy Modernization", + Description: "Transform legacy systems into modern, maintainable applications. Strangler fig pattern, domain-driven design, and incremental migration strategies.", + Icon: "refresh", + Features: []string{"Legacy Assessment", "Strangler Fig Migration", "Domain-Driven Design", "Data Migration", "API Facade Pattern", "Automated Testing"}, + Order: 5, + Active: true, + }, + { + ID: uuid.New(), + Title: "Technical Advisory & CTO Services", + Description: "Fractional CTO and technical advisory services for startups and scale-ups. Technology strategy, team building, and technical roadmap planning.", + Icon: "advisor", + Features: []string{"Technology Strategy", "Team Building & Hiring", "Technical Roadmaps", "Vendor Evaluation", "Architecture Decisions", "Investor Technical Due Diligence"}, + Order: 6, + Active: true, + }, + } + + for _, svc := range services { + s.services[svc.ID] = svc + } +} + +func (s *ServiceService) GetAll(ctx context.Context) ([]*model.Service, error) { + s.mu.RLock() + defer s.mu.RUnlock() + + var result []*model.Service + for _, svc := range s.services { + if svc.Active { + result = append(result, svc) + } + } + return result, nil +} + +func (s *ServiceService) GetByID(ctx context.Context, id uuid.UUID) (*model.Service, error) { + s.mu.RLock() + defer s.mu.RUnlock() + + svc, ok := s.services[id] + if !ok { + return nil, ErrNotFound + } + return svc, nil +} + +func (s *ServiceService) Create(ctx context.Context, svc *model.Service) (*model.Service, error) { + s.mu.Lock() + defer s.mu.Unlock() + + svc.ID = uuid.New() + s.services[svc.ID] = svc + return svc, nil +} + +func (s *ServiceService) Update(ctx context.Context, svc *model.Service) (*model.Service, error) { + s.mu.Lock() + defer s.mu.Unlock() + + if _, ok := s.services[svc.ID]; !ok { + return nil, ErrNotFound + } + s.services[svc.ID] = svc + return svc, nil +} + +func (s *ServiceService) Delete(ctx context.Context, id uuid.UUID) error { + s.mu.Lock() + defer s.mu.Unlock() + + if _, ok := s.services[id]; !ok { + return ErrNotFound + } + delete(s.services, id) + return nil +} diff --git a/internal/service/skill_service.go b/internal/service/skill_service.go new file mode 100644 index 0000000..214842b --- /dev/null +++ b/internal/service/skill_service.go @@ -0,0 +1,106 @@ +package service + +import ( + "context" + "sync" + + "github.com/dnoulet/go-first/internal/model" + "github.com/google/uuid" +) + +type SkillService struct { + mu sync.RWMutex + skills map[uuid.UUID]*model.Skill +} + +func NewSkillService() *SkillService { + s := &SkillService{ + skills: make(map[uuid.UUID]*model.Skill), + } + s.seedData() + return s +} + +func (s *SkillService) seedData() { + skills := []*model.Skill{ + {ID: uuid.New(), Name: "Go", Category: "Backend", Proficiency: 5, Icon: "go", Order: 1}, + {ID: uuid.New(), Name: "Java", Category: "Backend", Proficiency: 5, Icon: "java", Order: 2}, + {ID: uuid.New(), Name: "C#", Category: "Backend", Proficiency: 4, Icon: "csharp", Order: 3}, + {ID: uuid.New(), Name: "Python", Category: "Backend", Proficiency: 4, Icon: "python", Order: 4}, + {ID: uuid.New(), Name: "Ruby", Category: "Backend", Proficiency: 3, Icon: "ruby", Order: 5}, + {ID: uuid.New(), Name: "Kotlin", Category: "Backend", Proficiency: 4, Icon: "kotlin", Order: 6}, + {ID: uuid.New(), Name: "REST APIs", Category: "Architecture", Proficiency: 5, Icon: "api", Order: 7}, + {ID: uuid.New(), Name: "Microservices", Category: "Architecture", Proficiency: 5, Icon: "microservices", Order: 8}, + {ID: uuid.New(), Name: "System Design", Category: "Architecture", Proficiency: 5, Icon: "architecture", Order: 9}, + {ID: uuid.New(), Name: "Infrastructure", Category: "DevOps", Proficiency: 4, Icon: "infrastructure", Order: 10}, + {ID: uuid.New(), Name: "Docker", Category: "DevOps", Proficiency: 5, Icon: "docker", Order: 11}, + {ID: uuid.New(), Name: "Kubernetes", Category: "DevOps", Proficiency: 4, Icon: "kubernetes", Order: 12}, + {ID: uuid.New(), Name: "React", Category: "Frontend", Proficiency: 4, Icon: "react", Order: 13}, + {ID: uuid.New(), Name: "TypeScript", Category: "Frontend", Proficiency: 4, Icon: "typescript", Order: 14}, + {ID: uuid.New(), Name: "PostgreSQL", Category: "Database", Proficiency: 5, Icon: "postgresql", Order: 15}, + {ID: uuid.New(), Name: "Redis", Category: "Database", Proficiency: 4, Icon: "redis", Order: 16}, + {ID: uuid.New(), Name: "MongoDB", Category: "Database", Proficiency: 3, Icon: "mongodb", Order: 17}, + {ID: uuid.New(), Name: "GraphQL", Category: "API", Proficiency: 4, Icon: "graphql", Order: 18}, + {ID: uuid.New(), Name: "gRPC", Category: "API", Proficiency: 4, Icon: "grpc", Order: 19}, + {ID: uuid.New(), Name: "CI/CD", Category: "DevOps", Proficiency: 5, Icon: "cicd", Order: 20}, + } + + for _, skill := range skills { + s.skills[skill.ID] = skill + } +} + +func (s *SkillService) GetAll(ctx context.Context, category string) ([]*model.Skill, error) { + s.mu.RLock() + defer s.mu.RUnlock() + + var result []*model.Skill + for _, skill := range s.skills { + if category == "" || skill.Category == category { + result = append(result, skill) + } + } + return result, nil +} + +func (s *SkillService) GetByID(ctx context.Context, id uuid.UUID) (*model.Skill, error) { + s.mu.RLock() + defer s.mu.RUnlock() + + skill, ok := s.skills[id] + if !ok { + return nil, ErrNotFound + } + return skill, nil +} + +func (s *SkillService) Create(ctx context.Context, skill *model.Skill) (*model.Skill, error) { + s.mu.Lock() + defer s.mu.Unlock() + + skill.ID = uuid.New() + s.skills[skill.ID] = skill + return skill, nil +} + +func (s *SkillService) Update(ctx context.Context, skill *model.Skill) (*model.Skill, error) { + s.mu.Lock() + defer s.mu.Unlock() + + if _, ok := s.skills[skill.ID]; !ok { + return nil, ErrNotFound + } + s.skills[skill.ID] = skill + return skill, nil +} + +func (s *SkillService) Delete(ctx context.Context, id uuid.UUID) error { + s.mu.Lock() + defer s.mu.Unlock() + + if _, ok := s.skills[id]; !ok { + return ErrNotFound + } + delete(s.skills, id) + return nil +} diff --git a/projects.json b/projects.json new file mode 100644 index 0000000..388ea26 --- /dev/null +++ b/projects.json @@ -0,0 +1,146 @@ +[ + { + "id": "go-bananas", + "title": "go-bananas", + "description": "Runtime design-token service. Author design-token themes in a web interface, serve them to any application as CSS custom properties at run time. Consumers install nothing and have no build step. A theme is a document in a database — republishing a theme is a database write; every application picks it up on its next request. No package to version, no dependency to resolve, no build to re-run.", + "shortDesc": "Runtime design-token service serving CSS custom properties via HTTP", + "techStack": ["Go", "SQLite", "CSS Custom Properties", "Docker", "TypeScript", "React"], + "imageUrl": "/images/projects/go-bananas.png", + "demoUrl": "https://bananas.urpi.be", + "repoUrl": "https://forgejo.urpi.be/dnoulet/go-bananas", + "category": "Design System", + "featured": true, + "order": 1, + "tags": ["design-tokens", "css-custom-properties", "runtime-theming", "design-system", "zero-build"], + "url": "https://bananas.urpi.be", + "tenantUrlPattern": null + }, + { + "id": "go-first", + "title": "go-first", + "description": "URPI's main web application and company portfolio. A modern, full-stack web application showcasing expertise in web application and backend development. Built with Go, React, TypeScript, and deployed on cloud-native infrastructure. Features RESTful API for skills, projects, experience, services, and contact forms.", + "shortDesc": "URPI company portfolio and web application showcase", + "techStack": ["Go", "React", "TypeScript", "Tailwind CSS", "PostgreSQL", "Redis", "Docker", "Kubernetes", "Gin", "Vite"], + "imageUrl": "/images/projects/go-first.png", + "demoUrl": "https://urpi.be", + "repoUrl": "https://forgejo.urpi.be/dnoulet/go-first", + "category": "Full Stack", + "featured": true, + "order": 2, + "tags": ["portfolio", "company-website", "full-stack", "go", "react", "typescript", "tailwind"], + "url": "https://urpi.be", + "tenantUrlPattern": null + }, + { + "id": "go-guess", + "title": "go-guess", + "description": "Recruitment workspace for interviewers to define job profiles and a reusable question library, manage participants, extract known traits from uploaded CVs, invite candidates who match at least 60% of a posting's labels and skills, and run timed assessments followed by collaborative interviews. Multi-tenant architecture with Go Loose authentication.", + "shortDesc": "Multi-tenant recruitment platform with CV parsing, assessments, and collaborative interviews", + "techStack": ["Go", "React", "TypeScript", "PostgreSQL", "RabbitMQ", "GORM", "Chi", "Goose", "JWT", "Vite", "Go Loose"], + "imageUrl": "/images/projects/go-guess.png", + "demoUrl": "https://nmbs.guess.urpi.be", + "repoUrl": "https://forgejo.urpi.be/dnoulet/go-guess", + "category": "SaaS / HR Tech", + "featured": true, + "order": 3, + "tags": ["recruitment", "multi-tenant", "cv-parsing", "assessments", "interviews", "go-loose", "sso"], + "url": "https://nmbs.guess.urpi.be", + "tenantUrlPattern": "https://{tenant}.guess.urpi.be" + }, + { + "id": "go-loose", + "title": "go-loose", + "description": "Multi-tenant API-key control plane for Go HTTP services. Web console to register tenant applications, issue and revoke keys, import OpenAPI contracts, and inspect API metadata. Applications use the public client package as fail-closed middleware: requests without a valid key never reach the protected handler. Supports OIDC/SSO, tenant isolation, and audit logging.", + "shortDesc": "Multi-tenant API key management and authentication control plane", + "techStack": ["Go", "React", "TypeScript", "PostgreSQL", "OIDC", "OpenAPI", "JWT", "Chi", "GORM", "Docker", "Kubernetes"], + "imageUrl": "/images/projects/go-loose.png", + "demoUrl": "https://auth.urpi.be", + "repoUrl": "https://forgejo.urpi.be/dnoulet/go-loose", + "category": "Infrastructure / Auth", + "featured": true, + "order": 4, + "tags": ["api-keys", "multi-tenant", "auth", "sso", "oidc", "openapi", "control-plane", "middleware"], + "url": "https://auth.urpi.be", + "tenantUrlPattern": "https://{tenant}.auth.urpi.be" + }, + { + "id": "go-mad", + "title": "go-mad", + "description": "Web application for managing tenant database connections and running migration scripts. Helps administrators manage database connections for multiple tenants with a beautiful React UI. Features tenant management, application registration, database connection tracking, and goose-style SQL migration execution per tenant database.", + "shortDesc": "Multi-tenant database connection and migration management platform", + "techStack": ["Go", "React", "PostgreSQL", "Goose", "JWT", "Docker", "Helm", "Kubernetes", "TypeScript", "Vite"], + "imageUrl": "/images/projects/go-mad.png", + "demoUrl": "https://mad.urpi.be", + "repoUrl": "https://forgejo.urpi.be/dnoulet/go-mad", + "category": "DevOps / Database", + "featured": true, + "order": 5, + "tags": ["database-management", "migrations", "multi-tenant", "goose", "devops", "tenant-management"], + "url": "https://mad.urpi.be", + "tenantUrlPattern": null + }, + { + "id": "go-places", + "title": "go-places", + "description": "Travel log service with GraphQL API and image analysis. Authenticates callers against Go Loose SSO. Features place management, picture uploads with inline EXIF analysis and badge generation (Water, Sky, Vivid, Nature), coordinate-based linking of related places and images, and REST/GraphQL endpoints. SQLite-backed with persistent volume on Kubernetes.", + "shortDesc": "GraphQL travel log with image analysis, EXIF parsing, and location-based linking", + "techStack": ["Go", "React", "TypeScript", "GraphQL", "SQLite", "Go Loose", "Docker", "Kubernetes", "Helm", "EXIF", "Image Analysis"], + "imageUrl": "/images/projects/go-places.png", + "demoUrl": null, + "repoUrl": "https://forgejo.urpi.be/dnoulet/go-places", + "category": "Travel / Media", + "featured": true, + "order": 6, + "tags": ["travel-log", "graphql", "image-analysis", "exif", "location-based", "sqlite", "go-loose"], + "url": null, + "tenantUrlPattern": null + }, + { + "id": "go-tell", + "title": "go-tell", + "description": "Small content CMS: one Go binary that serves the API and embeds the React frontend, backed by a JSON file. Uses Go Loose for authentication to decide who is allowed to edit. Content is seeded in memory and can be mirrored to a JSON file. Single-host deployment at tell.dev with anonymous read access and authenticated writes.", + "shortDesc": "Embedded CMS with Go Loose authentication, JSON file storage", + "techStack": ["Go", "React", "TypeScript", "JSON Storage", "Go Loose", "OIDC", "Docker", "Single Binary"], + "imageUrl": "/images/projects/go-tell.png", + "demoUrl": "https://tell.urpi.be", + "repoUrl": "https://forgejo.urpi.be/dnoulet/go-tell", + "category": "CMS", + "featured": true, + "order": 7, + "tags": ["cms", "embedded", "json-storage", "go-loose", "auth", "single-binary"], + "url": "https://tell.urpi.be", + "tenantUrlPattern": null + }, + { + "id": "go-fish", + "title": "go-fish", + "description": "Web-based implementation of the classic Go Fish card game. Single-page application with waiting room, matchmaking, and real-time gameplay. Players join a waiting room, see available opponents (green) vs busy ones (red), click to match, and play Go Fish with standard rules. Collect books (4 of a kind), first to 13 wins.", + "shortDesc": "Real-time multiplayer Go Fish card game with waiting room and matchmaking", + "techStack": ["Go", "WebSockets", "HTML/CSS/JS", "Docker"], + "imageUrl": "/images/projects/go-fish.png", + "demoUrl": null, + "repoUrl": "https://forgejo.urpi.be/dnoulet/go-fish", + "category": "Games", + "featured": false, + "order": 8, + "tags": ["game", "multiplayer", "websockets", "go-fish", "cards", "real-time"], + "url": null, + "tenantUrlPattern": null + }, + { + "id": "go-doc", + "title": "go-doc", + "description": "Documentation and feedback platform for application teams. Authors manage Markdown documents in a React editor, while readers browse role-focused guidance for users, operators, and developers and leave audience-badged comments. The Go API stores documents and feedback as flat files, supports Go Loose authentication with tenant-aware comment visibility, and can serve the embedded frontend as a self-contained application.", + "shortDesc": "Markdown documentation portal with audience-focused feedback and tenant-aware access", + "techStack": ["Go", "React", "JavaScript", "Markdown", "net/http", "Go Loose", "Docker"], + "imageUrl": "/images/projects/go-doc.png", + "demoUrl": "https://doc.urpi.be", + "repoUrl": "https://forgejo.urpi.be/dnoulet/go-doc", + "category": "Documentation / Collaboration", + "featured": false, + "order": 9, + "tags": ["documentation", "markdown", "feedback", "comments", "audience-based", "go-loose", "multi-tenant"], + "url": "https://doc.urpi.be", + "tenantUrlPattern": null + } +] diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..cc991ec --- /dev/null +++ b/web/index.html @@ -0,0 +1,18 @@ + + + + + + + + + URPI | Web Application & Backend Development + + + + + +
+ + + \ No newline at end of file diff --git a/web/package-lock.json b/web/package-lock.json new file mode 100644 index 0000000..15c1e45 --- /dev/null +++ b/web/package-lock.json @@ -0,0 +1,6018 @@ +{ + "name": "go-first-frontend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "go-first-frontend", + "version": "1.0.0", + "dependencies": { + "clsx": "^2.1.1", + "lucide-react": "^0.436.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^6.26.1", + "tailwind-merge": "^2.5.2" + }, + "devDependencies": { + "@playwright/test": "^1.63.0", + "@testing-library/dom": "^10.4.2", + "@testing-library/jest-dom": "^7.0.1", + "@testing-library/react": "^16.3.3", + "@testing-library/user-event": "^14.6.7", + "@types/react": "^18.3.5", + "@types/react-dom": "^18.3.0", + "@typescript-eslint/eslint-plugin": "^8.3.0", + "@typescript-eslint/parser": "^8.3.0", + "@vitejs/plugin-react": "^4.3.1", + "autoprefixer": "^10.4.20", + "eslint": "^9.9.1", + "eslint-plugin-react-hooks": "^5.1.0-rc.0", + "eslint-plugin-react-refresh": "^0.4.11", + "happy-dom": "^20.14.5", + "jsdom": "^30.1.2", + "postcss": "^8.4.47", + "prettier": "^3.3.3", + "prettier-plugin-tailwindcss": "^0.6.6", + "tailwindcss": "^3.4.10", + "typescript": "^5.5.4", + "vite": "^5.4.3", + "vitest": "^1.6.0" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.5.0.tgz", + "integrity": "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@alloc/quick-lru": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.3.0.tgz", + "integrity": "sha512-U4+70Pc5ZS9osnCBCE5Jha/ciHM+Yp+CNMNC/7HvYbNRk1Ldd+f7qO65W5qfhu/TCv+/ozljlXXe9Nj8419DMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-7.1.3.tgz", + "integrity": "sha512-1t1U8Cm3RBl9vh3RAZhjkPaduzLSvQJRToRrxpGR/eDRW99mOREleuu+mWfiFa795bUzjwht7NgphCKTs2ianQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^3.4.2", + "@csstools/css-color-parser": "^4.2.5", + "@csstools/css-parser-algorithms": "^4.0.2", + "@csstools/css-tokenizer": "^4.0.2", + "lru-cache": "^11.5.3" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-9.2.4.tgz", + "integrity": "sha512-YnVzxDxLaqL0t7Q4wfcgHZjg55Wm6y5EaO7Bhu3q9J7wq/8iw3PhsnWiOQhYekx4VexnbkgWpTT6n+QIV7kczw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bidi-js": "^1.1.0", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.5.3" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.9.tgz", + "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bramus/specificity": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.3.tgz", + "integrity": "sha512-0GEz8qJWokkUVYQYRukmkSHIFNoBH8QTreeHrAFbvz1XFYc3Y+puhMs8PQ5x8GNugiC2l5jvDBXKLbBIByeSBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.2.1" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.2.tgz", + "integrity": "sha512-grhRy3OKmniaAEKXMjua5z/EODX0MSqBGjunw8+j/3HQjOnahs2AGhvEOIYVUWcU6ScApbhLhVrQTX8XqrMrow==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.4.3.tgz", + "integrity": "sha512-iex20d8CHVkyvg6B7UKV7uHnI2Bqo9g+EFfT9E0y+GvTvhZ/DwONJ+9aKb1dlqm0ZiGsL5RXjp0fCoJYnkeDjA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.2", + "@csstools/css-tokenizer": "^4.0.2" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.2.6.tgz", + "integrity": "sha512-iiPQ3iRWwnJkeEn6RIu6SJPr7hYrLz6XZ9s/QZl+2/LI5KQVjpl2fdmDSZKuD4xP6GMmMPgHFFXg6k1Wkz0Trg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.1.2", + "@csstools/css-calc": "^3.4.3" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.2", + "@csstools/css-tokenizer": "^4.0.2" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.2.tgz", + "integrity": "sha512-40cSKyMvK+tq4qz6Awrlye2WGuOKt3FwPgtGg6KTfbHOWNw+Rk1rzbAtZnZ6IBhsY491HLRnDXwoyBAijmmILA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.2" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.15.tgz", + "integrity": "sha512-J0u7HkVl2nzSlhsiTOp4AmwcUQ3D+mGEEKfBy/7To5/y7F2OHwyLrXfrhR0SMgr4p5Lo+eaMVSeai24zUcBIxA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.2.tgz", + "integrity": "sha512-OoKoR0f76dCY666JlcbhmVTs2drYj1GUXZTYTcbUgJjh9Nv41aFfZ21bPQTERm5+L5cBDo466NltB2lplS5GBw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "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==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "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==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.7.tgz", + "integrity": "sha512-F42g89Qd5oAWtp0k0nnSrjziAKza7w8SVT4mStc18LZMaRb4J1HQAHLCalEtDCxrTuksx7NU9qsmeLwpOfPqWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.3.2", + "minimatch": "^3.1.5", + "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/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz", + "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "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==", + "dev": true, + "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/@exodus/bytes": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.16.0.tgz", + "integrity": "sha512-IcpW84uEn3N7ETtNZMlxKhfl6Pec8rUNGOTBtWbK1FKhJxIFAptZyVrvVRVBimAJxJCgc3PxepxkdWWG4DVzfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.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==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@playwright/test": { + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.63.0.tgz", + "integrity": "sha512-oxMK4vllB9RK5NQ2l1pq1IfOf2AvnEuj/vYGDj0H2nMtmtZpKtCwt/l00GEO6xjGfpBNAvjovvYdCm50dRQkpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.63.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@remix-run/router": { + "version": "1.23.4", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.4.tgz", + "integrity": "sha512-q7j5geK7xs3UJSdm9/iytUNclBnLmYx1EnSeCFXHPeutdqgIMeFeHtUZgS3EhlKxdBEAu8OwtJCwmLrEzpSs7Q==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.64.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.64.1.tgz", + "integrity": "sha512-QRU7UpoS9hd8Ij5I6fCVBxZ0UpdfAbZ5y7BWG5STFGMoM9jGzUP464yYAa3CzZxvG+jBwq1kI8L07xx0ReAdGA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.64.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.64.1.tgz", + "integrity": "sha512-ttQF6Rp4MrNy9/2AOPmHmMXbNE8kIlNPYoGC3ksid9tonleMQALfrhTFJiQxobBvXYKG5Wcc8VGcHWTHR0ik8w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.64.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.64.1.tgz", + "integrity": "sha512-MvzZ+qbBi5jrafURHsASUF1qW/58tOJFt/gFVJo2znQWrKX4CSf/Lt/qGONtoA4yHsNL5QzgdXMuITZxAs62Vg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.64.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.64.1.tgz", + "integrity": "sha512-9uj1VBuiPK5ij/+g0mCK17X+V/4SbErvRQMXIQjZAtbl4VcUhv63RhPZsFSWHrCRkTd3FKDtaVTrtT0+8bIhhQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.64.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.64.1.tgz", + "integrity": "sha512-FCoXm836IqcXWhWPRkqlLx3Hjnv9S7c8tg5MFwsgHB3aFCz1JjUy6b1ssrxEjEbqaHpnvRmNjUUHxsXuU85NYA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.64.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.64.1.tgz", + "integrity": "sha512-Xa6Oxo86Ilx1hq1vNb69541NOuUyhGEH3Sc0g7eGkfsfRblWh4hAa4Lj2GWwZyfUVYL+Z4xK3TxhwIuZ3fNPLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.64.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.64.1.tgz", + "integrity": "sha512-Nkw7cVMu1yrczpzr42Mh1BZ5Ai5A4Fsz2Je/frbwHmazpQTJzP+qo7pNjdxHrxVj56ivKTJihNoN90OD1hGxBA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.64.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.64.1.tgz", + "integrity": "sha512-JONaLM9mZPXbSetxMzYG3496qoJuWvG2ERCTrrVptfw/Fwe4UzrQoEx/mYEeZCRv/Xz0RldOLFeqDfqmH75/Zg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.64.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.64.1.tgz", + "integrity": "sha512-OfimHma+prmdoobHI3tfYud2enQ2LXYaMu+iMUQH7IjJkykJ6qUmAs1M/scdQaEKRWysiKwTctsDa2LXvaO0xw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.64.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.64.1.tgz", + "integrity": "sha512-X5fwh2HM8EHbgfFwpf4m2x2pG/AHdQ2aPDPk+JCDwV4DPeM8yxYmKG1vvm123JSc9gBSu8tu9NECA99wHPztsw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.64.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.64.1.tgz", + "integrity": "sha512-8dp2TxKdjiX/4f7Da9gF7BR29hygxOQlHxKNa8qxJHoqZ13ytrogFTpMPhtVEFrgFu9MSOabAn006PeoygavjA==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.64.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.64.1.tgz", + "integrity": "sha512-YuYDpRSZnU9qLf6sqyhiBdMRCw5ImECGLNqRjM2hZrJIEwAckameWkRJ3SUnn0oxSewZIib/4iLwyEXj40QqQQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.64.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.64.1.tgz", + "integrity": "sha512-gGj3v7YYkysy7IbfclMI4PDtHaa6YX4nU1D5NR1fyvXAGqM4TDk28ZWvrpOkkK92eoQRryAludWz6jNjsm77Qg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.64.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.64.1.tgz", + "integrity": "sha512-DTZVwvS94FUYpP0CjCfz2iT0XTmBfQGc/arx9IdwUbDImpe9h8dWuMT/oPUvcGiwLKMQoNmuFBx3AVajm+FNjA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.64.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.64.1.tgz", + "integrity": "sha512-NWnyiq2moc8iEHGyTjIFadJTy8VCvCU/rUBfor+3l30JOQKAifpmKdxX4ZxRRPSPE/rLhFpLADdzNjS3Ldctdw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.64.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.64.1.tgz", + "integrity": "sha512-yIz+7L37g7bVOGLaUQmGqiYADH262krL4dtf5zU6QGldPgOairLEgW1FTs5pP0UFkHq5uf2qxDttOFsVAX6kxg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.64.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.64.1.tgz", + "integrity": "sha512-sRK+GWovJZV6ijyV/k1Ft3FL5+4DscF5OYsJolTbhVTTLFxKOPVjS0wxUUHakvL0vRw/cbFxZNeZLfM/EzuQhA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.64.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.64.1.tgz", + "integrity": "sha512-T2C0NXqqD063yJdm8UGdcoudBATDODdphEXWS7rEoh0jOITfoEBv7LHfRBYVsy+pI7wwxgScoZpkFiQOf40ryQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.64.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.64.1.tgz", + "integrity": "sha512-05wl8uRZMkcobB6YZrzskr2SiM0VOdzlFw3bAxOjKKTt3M6o3O0X/hE/8xk15u8oQY17xUCdTKcNMMg45ZAFog==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.64.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.64.1.tgz", + "integrity": "sha512-NoRZK1VclOgn9x3b9vPJ3sTxwvQ/F+Hf93xurRKSFJKiwNZyQZnLxVpqlCejhDvesMLMF8VNN+Y4jHTFddV/Xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.64.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.64.1.tgz", + "integrity": "sha512-SDkX4OkZBePUPf0/XsXVJLwS+O61CmkDKuIyk5CeWddK3vkE8AZjmWK+p1ILRez4wnbS5rTK+U9YmcbRyx2dHw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.64.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.64.1.tgz", + "integrity": "sha512-JQ7el/Plr3juYFxIVu9YXDPNl57XR2L8WIc87JX9wj0L3goV8fXQPQXb3DaHVXq61X/WCGWsTCQsQHo6/Mz5UQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.64.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.64.1.tgz", + "integrity": "sha512-oZSlQ2H3ZDOz3ce1jEyuOZKuD0tPY/D2cKGMcMl0jhHQauBvkHHKAGR1OeRiMluvu3itzLRU2xCTUZptLayObA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.64.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.64.1.tgz", + "integrity": "sha512-Do2m3jUeVAvUDpee/yVZYRqhO8Ecx4x5sX3EVGalqLx+OxecLDGx3sIwPEm1Nq+MFK2FV3pgdpwWP4624b85Og==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.64.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.64.1.tgz", + "integrity": "sha512-KpxCwRn1Qh3tmBHAqIAF4HtUCVKqZd0w57TsItWblx3QVeJLtXaSgeBFAj1WAvBSpBo+Mh9iZCjj6YW/HaBVMw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.12", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.12.tgz", + "integrity": "sha512-hhyNJ+nbR6ZR7pToHvllEFun9TL0sbL+tk/ON75lo+Xas054uez98qRbsuNt7MBCyZKK4+8Yli/OAGZhmfBZ/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/dom": { + "version": "10.4.2", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.2.tgz", + "integrity": "sha512-yzr2S9HyAIdhz2/6qHgbs665Q7PKVcDF05vsOlHPxG1mo36gKVesdYVeDLnXgfjJ03CrKRk08knc6+E/9m8v2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/dom/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@testing-library/dom/node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/@testing-library/dom/node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/dom/node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@testing-library/dom/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/jest-dom": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-7.0.1.tgz", + "integrity": "sha512-oMDTC3oA+6CXSO2JZnvOI7CA6oVub6kij5ggk9ohwye5slmkwxYDXcPOVxgMw/RQlticjtO0C1RZkR97HgrWMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=22", + "npm": ">=6", + "yarn": ">=1" + }, + "peerDependencies": { + "@testing-library/dom": ">=10 <11", + "vitest": ">= 0.32" + }, + "peerDependenciesMeta": { + "vitest": { + "optional": true + } + } + }, + "node_modules/@testing-library/react": { + "version": "16.3.3", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.3.tgz", + "integrity": "sha512-Uo193NgQbPMz6lrrhtRQQFcMC6Re/ELLFbbuVL30WDlZxlpZf9/lMHTAVxPRLw1q1iu9OJmR1c2BLiENRstdBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.6.7", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.7.tgz", + "integrity": "sha512-MPCpX8bxe8zS+JmmTwLp8jd0dy1rAm60Te/SL8JrQM3qvQJcBOs1d7IefJMyZzqM3EWBrDn/LWDt1BCGu4ASfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "26.6.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.4.tgz", + "integrity": "sha512-ldVPDCzj7fsaGZrLB0NuHuTvJcsNasysBAqMolr/cgxrLd1xbqxIr3XJiPnHHJUCxj5sNF1vnRj9aWnrVh5Jcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.9.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.31", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", + "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@types/whatwg-mimetype": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz", + "integrity": "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.2.tgz", + "integrity": "sha512-67MQl+fpWKVTT1NYdnmo3U4sc/xPo/zQBncVnI74qmQa0z/b+1g6iYqNmGCPbxO+zz2aklb08a0oHfegiVd0/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.71.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.71.1.tgz", + "integrity": "sha512-nNlBf6HuotqMgZvSkWD5hNdemiBIxQ2NmrlgfcUIeZRs+BbakjscCQRq/Hb74EFVnFiSWWdL12JruqwBy3ERDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.71.1", + "@typescript-eslint/type-utils": "8.71.1", + "@typescript-eslint/utils": "8.71.1", + "@typescript-eslint/visitor-keys": "8.71.1", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.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.71.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.71.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.71.1.tgz", + "integrity": "sha512-oM/5sAqz/l1v/zYK1qTOPrn2+JIrBGC1V6uNYucRO5gh6eT9vWJ/RhPbGE59byvZEUQ4VP6XkLBaS5Jti5jpvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.71.1", + "@typescript-eslint/types": "8.71.1", + "@typescript-eslint/typescript-estree": "8.71.1", + "@typescript-eslint/visitor-keys": "8.71.1", + "debug": "^4.4.3" + }, + "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 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.71.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.71.1.tgz", + "integrity": "sha512-Oijc9RUsohHjncg6iGi7gCtjnEjYt9FWv9u6ygK8uKcHK5eLjLAkqERQ9BaytbpceOFdk6kifPCS/zAZE+A99g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.71.1", + "@typescript-eslint/types": "^8.71.1", + "debug": "^4.4.3" + }, + "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.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.71.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.71.1.tgz", + "integrity": "sha512-1YUDZXdTnXLlob03SFHBipdyMBsDVm7zoSz0ytTJ39dk5J1TgsTK03VlR/dLXG/RLLUrPcYbD4rp23BmHxHP0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.71.1", + "@typescript-eslint/visitor-keys": "8.71.1" + }, + "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.71.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.71.1.tgz", + "integrity": "sha512-oTvkml5SxXhgg+WWKhod5qWQICDXtOB+/Hi9VdPKFAANJ2b7YOgMffgvzXuqog7R6A/JtrzRf128dsxummyWhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.71.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.71.1.tgz", + "integrity": "sha512-kH3t3jZCYv2PZSBEWPKTariLrCaoF1CLoPitYFBJOhAGEwWnSRAtGEifrfrpBa4K9JlwWNZotgA5w3/wLSk/FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.71.1", + "@typescript-eslint/typescript-estree": "8.71.1", + "@typescript-eslint/utils": "8.71.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.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 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.71.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.71.1.tgz", + "integrity": "sha512-ybYnPTUwg3VQQOFEiBPLs8Jxl1ry2vXdn1kVKguomBbvxpetdu3MKTNDqFNUrlG8ylP9MpZosmXMFYlGQHk1oQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.71.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.71.1.tgz", + "integrity": "sha512-pSKSEK1JJpHs6KiKVtABCE4iJK5d7FLFzDXQNBQ2oRY7LEd3UwOt3TZemTgu9syTIf56dJWlTn/pdmrO9oKUXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.71.1", + "@typescript-eslint/tsconfig-utils": "8.71.1", + "@typescript-eslint/types": "8.71.1", + "@typescript-eslint/visitor-keys": "8.71.1", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.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.1.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.71.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.71.1.tgz", + "integrity": "sha512-CBjT6gfAz3DW2j9Q4moFnqt208Aq+506sVGXfpRcyAeqn0EpayL0mjdWoCO2t3RC7GewNrFxdeehuEaPg6DMzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.71.1", + "@typescript-eslint/types": "8.71.1", + "@typescript-eslint/typescript-estree": "8.71.1" + }, + "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 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.71.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.71.1.tgz", + "integrity": "sha512-0GxiUGqMU0qXJt58cBsiG3DP8lUmBaFo2vChtUX+CT7PnR3v/VGLLrzVP4K9r1lNiZXjHYefMBbPunplr09v6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.71.1", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@vitest/expect": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.0.tgz", + "integrity": "sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "1.6.0", + "@vitest/utils": "1.6.0", + "chai": "^4.3.10" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.0.tgz", + "integrity": "sha512-P4xgwPjwesuBiHisAVz/LSSZtDjOTPYZVmNAnpHHSR6ONrf8eCJOFRvUwdHn30F5M1fxhqtl7QZQUk2dprIXAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "1.6.0", + "p-limit": "^5.0.0", + "pathe": "^1.1.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner/node_modules/p-limit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", + "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@vitest/runner/node_modules/yocto-queue": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@vitest/snapshot": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.0.tgz", + "integrity": "sha512-+Hx43f8Chus+DCmygqqfetcAZrDJwvTj0ymqjQq4CvmpKFSTVteEOBzCusu1x2tt4OJcvBflyHUE0DZSLgEMtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.0.tgz", + "integrity": "sha512-leUTap6B/cqi/bQkXUu6bQV5TZPx7pmMBKBQiI0rJA8c3pB56ZsaTbREnF7CJfmvAS4V2cXIBAh/3rVwrrCYgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^2.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.0.tgz", + "integrity": "sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "diff-sequences": "^29.6.3", + "estree-walker": "^3.0.3", + "loupe": "^2.3.7", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.19.0.tgz", + "integrity": "sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/autoprefixer": { + "version": "10.6.1", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.6.1.tgz", + "integrity": "sha512-cL1Qz6ADZhcEbny/8HPfe99J6HhNoYtpX2LFLIbhgGE7Q1hlQVkYFdetDN7Id3KiQxhDrHwzlHr/YQCnZ8+xSA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.9", + "caniuse-lite": "^1.0.30001810", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.27", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.27.tgz", + "integrity": "sha512-ElY12DaROGuan+lMmZ8Cvo/ZUbXPe7Enc/9VU/b1T3Kp4dwytRcNdR8DoSJN5SNJT/CuvcCA0DHDVmMOCePdRQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bidi-js": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.1.0.tgz", + "integrity": "sha512-fX1Onk0tdVPC7obPWB5EbJ1z7NVhLq4m2xZLq2YXBkxzMXIGRpNMU88n0EPgWseKl12J7zXs7qrDxPK4sRs2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.29.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.3.tgz", + "integrity": "sha512-1R4kiYKXGViqEN0CnoDrXc1StD9niAwu+j2dukWzrD4bJgsD4lDmEp0CRbc6E/vYJIfTHwPmwyaKtVSudICdPA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.11.26", + "caniuse-lite": "^1.0.30001813", + "electron-to-chromium": "^1.5.439", + "node-releases": "^2.0.57", + "update-browserslist-db": "^1.3.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-image-size": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/buffer-image-size/-/buffer-image-size-0.6.4.tgz", + "integrity": "sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001814", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001814.tgz", + "integrity": "sha512-/Uaf1lAzr59XcMpW0o96WoEfr+VXK2OX4U9AgFoiSHsVJ4HppnIFUjtYzsyDH2+tgANaQb2/oxYGwCPapN1FpA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", + "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/data-urls": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-8.0.0.tgz", + "integrity": "sha512-JQUCCK+/CAGbGGx618PlFZMXH81Jtj7t73CHv9dd/zVWvxgGT2zLFRjnKb+Ng46GIwcy+f7f88qd4duqllK+WQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^17.1.2" + }, + "engines": { + "node": "^22.14.0 || >=24.0.0" + } + }, + "node_modules/data-urls/node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/deep-eql": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", + "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.449", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.449.tgz", + "integrity": "sha512-dKeYk/3SFQz+nXi5whp3OuJzeWABEG4zCtkWJ9+Vby1E1iFM2y0gElRtQvNXRPJTsj8WAF4HMP7hvP790QtJ9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.5.tgz", + "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.6", + "@eslint/js": "9.39.5", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "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.5", + "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-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.26", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", + "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.3.tgz", + "integrity": "sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "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==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/happy-dom": { + "version": "20.14.5", + "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-20.14.5.tgz", + "integrity": "sha512-x/RzkpWO40bTjIoT30iQtt64FLLmH/iRcUCN2X//bLx7H3ifkdfPXyqsro/OYtqzIAhiLMMA7mmiOR9C3NOKjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": ">=20.0.0", + "@types/whatwg-mimetype": "^3.0.2", + "@types/ws": "^8.18.1", + "buffer-image-size": "^0.6.4", + "entities": "^7.0.1", + "whatwg-mimetype": "^3.0.0", + "ws": "^8.21.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-7.0.0.tgz", + "integrity": "sha512-UikN5yr7xsCDAq87Or5or0PAlD3HJJOKVzM05az588WnpDJ4Ux7a2A53Qi6gofGg2/EtvF/H4hCi/TXfCW4Y6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.15.1" + }, + "engines": { + "node": "^22.13.0 || >=24.0.0" + } + }, + "node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/ignore": { + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.12.tgz", + "integrity": "sha512-/8UvqAPU9DGTI9k4mxtf49U37Isfwr8Uts96+SBHIkFxPJnHS0Ew4f00sM4Scd8V8EjM0jUNtVDdv1kPdt35lg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.17.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.17.0.tgz", + "integrity": "sha512-J/vG0zBCbIKOQFfufSwyXdMrsohyJIUNkrnmo6WZGzoM7tr/lsbfW5b2BvisL6zsyMzK9UxV9L6c7AoFbyXHOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdom": { + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-30.1.2.tgz", + "integrity": "sha512-0FFE/jE1rppmVfUrJUgxqXjcwolZYIGtAgj1pTussMhNZxIxi7W/PvfWaMNroGi2B36X1IKHbexpZ9DhkoOPiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^7.1.3", + "@asamuzakjp/dom-selector": "^9.2.4", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.15", + "@exodus/bytes": "^1.16.0", + "css-tree": "^3.2.1", + "data-urls": "^8.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^7.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.5.3", + "parse5": "^8.0.1", + "saxes": "^6.0.0", + "tough-cookie": "^6.0.2", + "undici": "^8.11.2", + "w3c-xmlserializer": "^6.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^17.2.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + }, + "peerDependencies": { + "canvas": "^3.2.3" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/lru-cache": { + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/jsdom/node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/local-pkg": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.1.tgz", + "integrity": "sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mlly": "^1.7.3", + "pkg-types": "^1.2.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.1" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.436.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.436.0.tgz", + "integrity": "sha512-N292bIxoqm1aObAg0MzFtvhYwgQE6qnIOWx/GLj5ONgcTPH6N0fD9bVq/GfdeC9ZORBXozt/XeEKDpiB3x3vlQ==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "bin": { + "lz-string": "bin/bin.js" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mlly": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, + "node_modules/mlly/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.57", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.57.tgz", + "integrity": "sha512-kQK9LGGFiHtrWiNhZtA7Qbw17AQz+dmsEKODRIVTXA9+e5MS/2gZEBhYJt13GrAz5/IOZKddH/0Z3TP/Zgo+yw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.1.0.tgz", + "integrity": "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/pkg-types/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/playwright": { + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.63.0.tgz", + "integrity": "sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.63.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/playwright-core": { + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.63.0.tgz", + "integrity": "sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/postcss": { + "version": "8.5.29", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.29.tgz", + "integrity": "sha512-49cGhUbXj8Qenv0iTMxA1cFBzxXoctpC9Ujd77t1WcbJIr6nF/eI7g/8MgxrYldFRuAXvja7xQRwavoW7kgrxQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.19", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", + "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.9.9", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.9.tgz", + "integrity": "sha512-Z/CJHIkdujO/OtN7nXUii0Rf3VT5SRuhjBA82Xvu2XhBUgX3nhP67T0LHceBdQLex7OOFGTox+Q5Yg8Jk2Qivg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-plugin-tailwindcss": { + "version": "0.6.14", + "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.14.tgz", + "integrity": "sha512-pi2e/+ZygeIqntN+vC573BcW5Cve8zUB0SSAGxqpB4f96boZF4M3phPVoOFCeypwkpRYdi7+jQ5YJJUwrkGUAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.21.3" + }, + "peerDependencies": { + "@ianvs/prettier-plugin-sort-imports": "*", + "@prettier/plugin-hermes": "*", + "@prettier/plugin-oxc": "*", + "@prettier/plugin-pug": "*", + "@shopify/prettier-plugin-liquid": "*", + "@trivago/prettier-plugin-sort-imports": "*", + "@zackad/prettier-plugin-twig": "*", + "prettier": "^3.0", + "prettier-plugin-astro": "*", + "prettier-plugin-css-order": "*", + "prettier-plugin-import-sort": "*", + "prettier-plugin-jsdoc": "*", + "prettier-plugin-marko": "*", + "prettier-plugin-multiline-arrays": "*", + "prettier-plugin-organize-attributes": "*", + "prettier-plugin-organize-imports": "*", + "prettier-plugin-sort-imports": "*", + "prettier-plugin-style-order": "*", + "prettier-plugin-svelte": "*" + }, + "peerDependenciesMeta": { + "@ianvs/prettier-plugin-sort-imports": { + "optional": true + }, + "@prettier/plugin-hermes": { + "optional": true + }, + "@prettier/plugin-oxc": { + "optional": true + }, + "@prettier/plugin-pug": { + "optional": true + }, + "@shopify/prettier-plugin-liquid": { + "optional": true + }, + "@trivago/prettier-plugin-sort-imports": { + "optional": true + }, + "@zackad/prettier-plugin-twig": { + "optional": true + }, + "prettier-plugin-astro": { + "optional": true + }, + "prettier-plugin-css-order": { + "optional": true + }, + "prettier-plugin-import-sort": { + "optional": true + }, + "prettier-plugin-jsdoc": { + "optional": true + }, + "prettier-plugin-marko": { + "optional": true + }, + "prettier-plugin-multiline-arrays": { + "optional": true + }, + "prettier-plugin-organize-attributes": { + "optional": true + }, + "prettier-plugin-organize-imports": { + "optional": true + }, + "prettier-plugin-sort-imports": { + "optional": true + }, + "prettier-plugin-style-order": { + "optional": true + }, + "prettier-plugin-svelte": { + "optional": true + } + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "6.30.6", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.6.tgz", + "integrity": "sha512-5HfK7k5im7LTOB0EqCQmfvy4C13G92Ssj1VTmouTK3AJvyjKTnFuCV0vcMAD/JS+JC4DvDIBRrlAeJIFjh5VWg==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.4" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.30.6", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.6.tgz", + "integrity": "sha512-0RHKZz7wwffvkU+2MFVT2NnjK44ssLEV+m0CAJaS2Ksmorrwj7WxH00jO0SOCW26/tINUnJHToXblDs33I38YQ==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.4", + "react-router": "6.30.6" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/read-cache": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.2.tgz", + "integrity": "sha512-/peqiBB/n07gQGLsWaHho3WfvUyRscw0gYTsEFMhrIe/nWLkYaf5SbKYjGYqtRV3aPwykJgF2VEMo1ac4bnsGA==", + "dev": true, + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.13", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.13.tgz", + "integrity": "sha512-Dj/cW9zBV2aRj6XzCxuGIs3ESZKv02jixne3q6jFZYZ3/Tg09gHHviSMDL4kObh9pqSahdRHEnDoaP2gn8YLhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.17.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.64.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.64.1.tgz", + "integrity": "sha512-7s4V+3J8ux75WNj9qwSJnrWmlCmZ2YHr2fojtHy5RiIMvhJv6DGe7HKKz7L6G1sq1Eg1fLC8SjYtAtDis1F4CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.64.1", + "@rollup/rollup-android-arm64": "4.64.1", + "@rollup/rollup-darwin-arm64": "4.64.1", + "@rollup/rollup-darwin-x64": "4.64.1", + "@rollup/rollup-freebsd-arm64": "4.64.1", + "@rollup/rollup-freebsd-x64": "4.64.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.64.1", + "@rollup/rollup-linux-arm-musleabihf": "4.64.1", + "@rollup/rollup-linux-arm64-gnu": "4.64.1", + "@rollup/rollup-linux-arm64-musl": "4.64.1", + "@rollup/rollup-linux-loong64-gnu": "4.64.1", + "@rollup/rollup-linux-loong64-musl": "4.64.1", + "@rollup/rollup-linux-ppc64-gnu": "4.64.1", + "@rollup/rollup-linux-ppc64-musl": "4.64.1", + "@rollup/rollup-linux-riscv64-gnu": "4.64.1", + "@rollup/rollup-linux-riscv64-musl": "4.64.1", + "@rollup/rollup-linux-s390x-gnu": "4.64.1", + "@rollup/rollup-linux-x64-gnu": "4.64.1", + "@rollup/rollup-linux-x64-musl": "4.64.1", + "@rollup/rollup-openbsd-x64": "4.64.1", + "@rollup/rollup-openharmony-arm64": "4.64.1", + "@rollup/rollup-win32-arm64-msvc": "4.64.1", + "@rollup/rollup-win32-ia32-msvc": "4.64.1", + "@rollup/rollup-win32-x64-gnu": "4.64.1", + "@rollup/rollup-win32-x64-msvc": "4.64.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map-js": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-literal": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.1.1.tgz", + "integrity": "sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwind-merge": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.1.tgz", + "integrity": "sha512-Oo6tHdpZsGpkKG88HJ8RR1rg/RdnEkQEfMoEk2x1XRI3F1AxeU+ijRXpiVUF4UbLfcxxRGw6TbUINKYdWVsQTQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tinypool": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.4.tgz", + "integrity": "sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", + "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.4.16", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.16.tgz", + "integrity": "sha512-QwBER5KMR86IIjpIiO7H/Z3IMJPsZ1A6RKPAqzTTgOyUQUSt9FdnKcqhTaJmkY6HVrgouZHZR0ncK5QxvmnQeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.16" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.4.16", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.16.tgz", + "integrity": "sha512-MDolfaSJtlSK5Y0A1xl3277ekubZwobpBjugknDizI9O5Rm60a1m8k4ICK+MRsCDzPygT81mp3BBf5RKDlFRfA==", + "dev": true, + "license": "MIT" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tough-cookie": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz", + "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-7.0.0.tgz", + "integrity": "sha512-BcDSDW+SIGWVOns1Zm6Yg91q9AEj7eaID0QDY9oIqJD1A8dscNwMcl9Sms2DeuWCNov1X0/oXanHGYQBSLwZsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": "^22.14.0 || >=24.0.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.11.2.tgz", + "integrity": "sha512-u4UB2/IrKdU6lFxumHmmo1a3fCQO5tzQllRorfoRS63txhrB7xTpSn1PftwC4qEHkOaqP95fCWW4lJzwErwzhQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/undici-types": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", + "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.3.tgz", + "integrity": "sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.0.tgz", + "integrity": "sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.4", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.0.tgz", + "integrity": "sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "1.6.0", + "@vitest/runner": "1.6.0", + "@vitest/snapshot": "1.6.0", + "@vitest/spy": "1.6.0", + "@vitest/utils": "1.6.0", + "acorn-walk": "^8.3.2", + "chai": "^4.3.10", + "debug": "^4.3.4", + "execa": "^8.0.1", + "local-pkg": "^0.5.0", + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "std-env": "^3.5.0", + "strip-literal": "^2.0.0", + "tinybench": "^2.5.1", + "tinypool": "^0.8.3", + "vite": "^5.0.0", + "vite-node": "1.6.0", + "why-is-node-running": "^2.2.2" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "1.6.0", + "@vitest/ui": "1.6.0", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-6.0.0.tgz", + "integrity": "sha512-4Nsy8K5Tr6SPDH9jhKJOHf7ChDrc1zufZTVSF7x72hwuEXBqxqk9G6cK+K2NRUtB3iELRJqjXb4JPDMBjMTl2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "17.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-17.2.0.tgz", + "integrity": "sha512-IhLIHNcBrzIixC5XXsbr4DxdjajyA5vQ5RwGZMtrUp+GbtE5Nrsge9QKau7jXrC0aTXLYGV0xiW7Tb4YS2aomw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.16.0", + "tr46": "^7.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^22.14.0 || >=24.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ws": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz", + "integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000..26ac83a --- /dev/null +++ b/web/package.json @@ -0,0 +1,52 @@ +{ + "name": "go-first-frontend", + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "format": "prettier --write \"src/**/*.{ts,tsx,css,md}\"", + "test": "vitest run", + "test:ui": "vitest --ui", + "test:watch": "vitest", + "test:coverage": "vitest run --coverage", + "test:e2e": "playwright test", + "test:e2e:ui": "playwright test --ui", + "test:all": "npm run test && npm run test:e2e" + }, + "dependencies": { + "clsx": "^2.1.1", + "lucide-react": "^0.436.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^6.26.1", + "tailwind-merge": "^2.5.2" + }, + "devDependencies": { + "@playwright/test": "^1.63.0", + "@testing-library/dom": "^10.4.2", + "@testing-library/jest-dom": "^7.0.1", + "@testing-library/react": "^16.3.3", + "@testing-library/user-event": "^14.6.7", + "@types/react": "^18.3.5", + "@types/react-dom": "^18.3.0", + "@typescript-eslint/eslint-plugin": "^8.3.0", + "@typescript-eslint/parser": "^8.3.0", + "@vitejs/plugin-react": "^4.3.1", + "autoprefixer": "^10.4.20", + "eslint": "^9.9.1", + "eslint-plugin-react-hooks": "^5.1.0-rc.0", + "eslint-plugin-react-refresh": "^0.4.11", + "happy-dom": "^20.14.5", + "jsdom": "^30.1.2", + "postcss": "^8.4.47", + "prettier": "^3.3.3", + "prettier-plugin-tailwindcss": "^0.6.6", + "tailwindcss": "^3.4.10", + "typescript": "^5.5.4", + "vite": "^5.4.3", + "vitest": "^1.6.0" + } +} diff --git a/web/postcss.config.js b/web/postcss.config.js new file mode 100644 index 0000000..e99ebc2 --- /dev/null +++ b/web/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} \ No newline at end of file diff --git a/web/public/favicon.svg b/web/public/favicon.svg new file mode 100644 index 0000000..219d7ba --- /dev/null +++ b/web/public/favicon.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/web/public/grid.svg b/web/public/grid.svg new file mode 100644 index 0000000..c8abac4 --- /dev/null +++ b/web/public/grid.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/web/src/App.tsx b/web/src/App.tsx new file mode 100644 index 0000000..ffc9aa2 --- /dev/null +++ b/web/src/App.tsx @@ -0,0 +1,27 @@ +import { Header } from './components/layout/Header' +import { Footer } from './components/layout/Footer' +import { Hero } from './components/sections/Hero' +import { Services } from './components/sections/Services' +import { Projects } from './components/sections/Projects' +import { Skills } from './components/sections/Skills' +import { Experience } from './components/sections/Experience' +import { Contact } from './components/sections/Contact' + +function App() { + return ( +
+
+
+ + + + + + +
+
+
+ ) +} + +export default App \ No newline at end of file diff --git a/web/src/__tests__/App.test.tsx b/web/src/__tests__/App.test.tsx new file mode 100644 index 0000000..5ad8a5c --- /dev/null +++ b/web/src/__tests__/App.test.tsx @@ -0,0 +1,129 @@ +import { render, screen, waitFor, act } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { BrowserRouter } from 'react-router-dom' +import { describe, it, expect, beforeEach } from 'vitest' +import App from '../App' +import { mockFetchResponse, resetFetchMock, getFetchCalls } from './api-mock' + +const renderApp = () => { + return render( + + + + ) +} + +const mockSkills = [ + { id: '1', name: 'Go', category: 'Backend', proficiency: 5, icon: 'go', order: 1, created_at: '', updated_at: '' }, + { id: '2', name: 'React', category: 'Frontend', proficiency: 4, icon: 'react', order: 2, created_at: '', updated_at: '' }, +] + +const mockProjects = [ + { id: '1', title: 'Test Project', description: 'Desc', short_desc: 'Short', tech_stack: ['Go', 'React'], image_url: '', demo_url: '', repo_url: '', category: 'Full Stack', featured: true, order: 1, created_at: '', updated_at: '' }, +] + +const mockExperience = [ + { id: '1', company: 'Test Co', role: 'Dev', location: 'Remote', start_date: '2020-01-01', end_date: null, current: true, description: 'Test', tech_stack: ['Go'], order: 1, created_at: '', updated_at: '' }, +] + +const mockServices = [ + { id: '1', title: 'Web Dev', description: 'Desc', icon: 'code', features: ['React', 'Go'], order: 1, active: true, created_at: '', updated_at: '' }, +] + +describe('App Integration Tests', () => { + beforeEach(() => { + resetFetchMock() + }) + + it('renders all sections without errors', async () => { + mockFetchResponse(mockSkills) + mockFetchResponse(mockProjects) + mockFetchResponse(mockProjects) + mockFetchResponse(mockExperience) + mockFetchResponse(mockServices) + + renderApp() + + await waitFor(() => { + expect(screen.getByText('Services & Expertise')).toBeInTheDocument() + expect(screen.getByText('Selected Projects')).toBeInTheDocument() + expect(screen.getByText('Technical Skills')).toBeInTheDocument() + expect(screen.getByText('Experience')).toBeInTheDocument() + expect(screen.getByText("Let's Build Something Great")).toBeInTheDocument() + }, { timeout: 15000 }) + }) + + it('does not make excessive API calls on initial render', async () => { + mockFetchResponse([]) + mockFetchResponse([]) + mockFetchResponse([]) + mockFetchResponse([]) + mockFetchResponse([]) + + renderApp() + + await waitFor(() => { + expect(screen.getByText('Services & Expertise')).toBeInTheDocument() + }, { timeout: 15000 }) + + const calls = getFetchCalls() + expect(calls.length).toBeLessThanOrEqual(6) + }) + + it('handles API errors gracefully', async () => { + ;(globalThis.fetch as unknown as { mockResolvedValueOnce: (value: unknown) => void }).mockResolvedValueOnce({ + ok: false, + json: () => Promise.resolve({ success: false, error: { code: 'INTERNAL_ERROR', message: 'Server error' } }), + }) + ;(globalThis.fetch as unknown as { mockResolvedValueOnce: (value: unknown) => void }).mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve({ success: true, data: [] }), + }) + mockFetchResponse([]) + mockFetchResponse([]) + mockFetchResponse([]) + + renderApp() + + await waitFor(() => { + expect(screen.getByText('Services & Expertise')).toBeInTheDocument() + }, { timeout: 15000 }) + + expect(screen.getByText('Selected Projects')).toBeInTheDocument() + }) + + it('contact form submission works', async () => { + mockFetchResponse([]) + mockFetchResponse([]) + mockFetchResponse([]) + mockFetchResponse([]) + mockFetchResponse([]) + mockFetchResponse({ id: '1', name: 'Test', email: 'test@test.com', subject: 'Test', message: 'Test message', status: 'new', created_at: '', updated_at: '' }) + + renderApp() + + await waitFor(() => { + expect(screen.getByText("Let's Build Something Great")).toBeInTheDocument() + }, { timeout: 15000 }) + + // Use getByPlaceholderText for more specific targeting + const nameInput = screen.getByPlaceholderText('Your name') + const emailInput = screen.getByPlaceholderText('your@email.com') + const subjectInput = screen.getByPlaceholderText("What's this about?") + const messageInput = screen.getByPlaceholderText("Tell us about your project, timeline, budget, or just say hello...") + const submitButton = screen.getByRole('button', { name: /send message/i }) + + await userEvent.type(nameInput, 'John Doe') + await userEvent.type(emailInput, 'john@example.com') + await userEvent.type(subjectInput, 'Project Inquiry') + await userEvent.type(messageInput, 'Hello, I would like to discuss a project.') + + await act(async () => { + await userEvent.click(submitButton) + }) + + await waitFor(() => { + expect(screen.getByText('Message Sent!')).toBeInTheDocument() + }, { timeout: 15000 }) + }) +}) \ No newline at end of file diff --git a/web/src/__tests__/Services.test.tsx b/web/src/__tests__/Services.test.tsx new file mode 100644 index 0000000..97afb13 --- /dev/null +++ b/web/src/__tests__/Services.test.tsx @@ -0,0 +1,86 @@ +import { render, screen, waitFor } from '@testing-library/react' +import { describe, it, expect, beforeEach } from 'vitest' +import { Services } from '../components/sections/Services' +import { mockFetchResponse, resetFetchMock, getFetchCalls } from './api-mock' + +const renderServices = () => { + return render() +} + +const mockServices = [ + { id: '1', title: 'Web Dev', description: 'Desc', icon: 'code', features: ['React', 'Go'], order: 1, active: true, created_at: '', updated_at: '' }, + { id: '2', title: 'Backend', description: 'Desc', icon: 'server', features: ['Go', 'Java'], order: 2, active: true, created_at: '', updated_at: '' }, +] + +describe('Services Section - Infinite Loop Detection', () => { + beforeEach(() => { + resetFetchMock() + }) + + it('fetches services only once on mount', async () => { + mockFetchResponse(mockServices) + + renderServices() + + await waitFor(() => { + expect(screen.getByText('Web Dev')).toBeInTheDocument() + }, { timeout: 10000 }) + + const initialCalls = getFetchCalls().length + + await new Promise(resolve => setTimeout(resolve, 100)) + + const finalCalls = getFetchCalls().length + + expect(finalCalls).toBe(initialCalls) + }) + + it('does not re-fetch when parent re-renders', async () => { + mockFetchResponse(mockServices) + + const { rerender } = renderServices() + + await waitFor(() => { + expect(screen.getByText('Web Dev')).toBeInTheDocument() + }, { timeout: 10000 }) + + const initialCalls = getFetchCalls().length + + rerender() + + await new Promise(resolve => setTimeout(resolve, 100)) + + const finalCalls = getFetchCalls().length + + expect(finalCalls).toBe(initialCalls) + }) + + it('shows loading skeleton initially', async () => { + ;(globalThis.fetch as unknown as { mockImplementation: (fn: () => Promise) => void }).mockImplementation( + () => new Promise(resolve => setTimeout(() => resolve({ + ok: true, + json: () => Promise.resolve({ success: true, data: mockServices }), + }), 100)) + ) + + renderServices() + + // Should show loading state briefly + await waitFor(() => { + expect(screen.getByText('Web Dev')).toBeInTheDocument() + }, { timeout: 5000 }) + }) + + it('shows error state on API failure', async () => { + ;(globalThis.fetch as unknown as { mockResolvedValueOnce: (value: unknown) => void }).mockResolvedValueOnce({ + ok: false, + json: () => Promise.resolve({ success: false, error: { code: 'INTERNAL_ERROR', message: 'Failed to load' } }), + }) + + renderServices() + + await waitFor(() => { + expect(screen.getByText(/failed to load services/i)).toBeInTheDocument() + }, { timeout: 10000 }) + }) +}) \ No newline at end of file diff --git a/web/src/__tests__/api-mock.ts b/web/src/__tests__/api-mock.ts new file mode 100644 index 0000000..ffa39d7 --- /dev/null +++ b/web/src/__tests__/api-mock.ts @@ -0,0 +1,65 @@ +export interface MockApiResponse { + success: boolean + data?: T + error?: { + code: string + message: string + details?: string + } + meta?: { + page: number + page_size: number + total: number + total_pages: number + } +} + +export function createMockResponse(data: T, meta?: MockApiResponse['meta']): MockApiResponse { + return { + success: true, + data, + meta, + } +} + +export function createMockError(code: string, message: string, details?: string): MockApiResponse { + return { + success: false, + error: { code, message, details }, + } +} + +export function mockFetchResponse(data: T, meta?: MockApiResponse['meta']): void { + const response = createMockResponse(data, meta) + ;(globalThis.fetch as unknown as { mockResolvedValueOnce: (value: unknown) => void }).mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve(response), + }) +} + +export function mockFetchError(code: string, message: string, details?: string): void { + const response = createMockError(code, message, details) + ;(globalThis.fetch as unknown as { mockResolvedValueOnce: (value: unknown) => void }).mockResolvedValueOnce({ + ok: false, + json: () => Promise.resolve(response), + }) +} + +export function mockFetchNetworkError(): void { + ;(globalThis.fetch as unknown as { mockRejectedValueOnce: (value: unknown) => void }).mockRejectedValueOnce(new Error('Network error')) +} + +export function getFetchCalls(): Array<{ url: string; options?: RequestInit }> { + const mockFetch = globalThis.fetch as unknown as { mock: { calls: Array<[string, RequestInit?]> } } + return mockFetch.mock.calls.map(([url, options]) => ({ url, options })) +} + +export function getLastFetchCall(): { url: string; options?: RequestInit } | undefined { + const calls = getFetchCalls() + return calls[calls.length - 1] +} + +export function resetFetchMock(): void { + const mockFetch = globalThis.fetch as unknown as { mockClear: () => void } + mockFetch.mockClear() +} \ No newline at end of file diff --git a/web/src/__tests__/performance.test.tsx b/web/src/__tests__/performance.test.tsx new file mode 100644 index 0000000..a16b31f --- /dev/null +++ b/web/src/__tests__/performance.test.tsx @@ -0,0 +1,130 @@ +import { render, screen, waitFor } from '@testing-library/react' +import { BrowserRouter } from 'react-router-dom' +import { describe, it, expect, beforeEach } from 'vitest' +import App from '../App' +import { mockFetchResponse, resetFetchMock, getFetchCalls } from './api-mock' + +const renderApp = () => { + return render( + + + + ) +} + +const mockSkills = [ + { id: '1', name: 'Go', category: 'Backend', proficiency: 5, icon: 'go', order: 1, created_at: '', updated_at: '' }, +] + +const mockProjects = [ + { id: '1', title: 'Test Project', description: 'Desc', short_desc: 'Short', tech_stack: ['Go', 'React'], image_url: '', demo_url: '', repo_url: '', category: 'Full Stack', featured: true, order: 1, created_at: '', updated_at: '' }, +] + +const mockExperience = [ + { id: '1', company: 'Test Co', role: 'Dev', location: 'Remote', start_date: '2020-01-01', end_date: null, current: true, description: 'Test', tech_stack: ['Go'], order: 1, created_at: '', updated_at: '' }, +] + +const mockServices = [ + { id: '1', title: 'Web Dev', description: 'Desc', icon: 'code', features: ['React', 'Go'], order: 1, active: true, created_at: '', updated_at: '' }, +] + +describe('Performance & Infinite Loop Detection', () => { + beforeEach(() => { + resetFetchMock() + }) + + it('fetches data only once on mount', async () => { + mockFetchResponse(mockSkills) + mockFetchResponse(mockProjects) + mockFetchResponse(mockProjects) + mockFetchResponse(mockExperience) + mockFetchResponse(mockServices) + + renderApp() + + await waitFor(() => { + expect(screen.getByText('Services & Expertise')).toBeInTheDocument() + }, { timeout: 15000 }) + + const initialCalls = getFetchCalls().length + + await new Promise(resolve => setTimeout(resolve, 100)) + + const finalCalls = getFetchCalls().length + + expect(finalCalls).toBe(initialCalls) + }) + + it('does not re-fetch on component re-render without dependency changes', async () => { + mockFetchResponse(mockSkills) + mockFetchResponse(mockProjects) + mockFetchResponse(mockProjects) + mockFetchResponse(mockExperience) + mockFetchResponse(mockServices) + + const { rerender } = renderApp() + + await waitFor(() => { + expect(screen.getByText('Services & Expertise')).toBeInTheDocument() + }, { timeout: 15000 }) + + const initialCalls = getFetchCalls().length + + rerender( + + + + ) + + await new Promise(resolve => setTimeout(resolve, 100)) + + const finalCalls = getFetchCalls().length + + expect(finalCalls).toBe(initialCalls) + }) + + it('does not make excessive fetch calls', async () => { + let callCount = 0 + ;(globalThis.fetch as unknown as { mockImplementation: (fn: () => Promise) => void }).mockImplementation(() => { + callCount++ + return Promise.resolve({ + ok: true, + json: () => Promise.resolve({ success: true, data: [] }), + }) + }) + + renderApp() + + await waitFor(() => { + expect(screen.getByText('Services & Expertise')).toBeInTheDocument() + }, { timeout: 15000 }) + + const initialCallCount = callCount + + await new Promise(resolve => setTimeout(resolve, 100)) + + expect(callCount).toBeLessThanOrEqual(initialCallCount + 1) + }) + + it('Skills section does not cause infinite re-fetches', async () => { + mockFetchResponse(mockSkills) + mockFetchResponse(mockProjects) + mockFetchResponse(mockProjects) + mockFetchResponse(mockExperience) + mockFetchResponse(mockServices) + + renderApp() + + await waitFor(() => { + expect(screen.getByText('Technical Skills')).toBeInTheDocument() + }, { timeout: 15000 }) + + const initialCalls = getFetchCalls().length + + await new Promise(resolve => setTimeout(resolve, 100)) + + const finalCalls = getFetchCalls().length + + expect(finalCalls).toBe(initialCalls) + }) +}) \ No newline at end of file diff --git a/web/src/__tests__/setup.ts b/web/src/__tests__/setup.ts new file mode 100644 index 0000000..f0b4191 --- /dev/null +++ b/web/src/__tests__/setup.ts @@ -0,0 +1,52 @@ +import '@testing-library/jest-dom' +import { vi } from 'vitest' + +// Mock fetch for API calls +globalThis.fetch = vi.fn() + +// Mock window.matchMedia +Object.defineProperty(window, 'matchMedia', { + writable: true, + value: vi.fn().mockImplementation(query => ({ + matches: false, + media: query, + onchange: null, + addListener: vi.fn(), + removeListener: vi.fn(), + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + dispatchEvent: vi.fn(), + })), +}) + +// Mock IntersectionObserver +global.IntersectionObserver = vi.fn().mockImplementation(() => ({ + observe: vi.fn(), + unobserve: vi.fn(), + disconnect: vi.fn(), +})) + +// Mock ResizeObserver +global.ResizeObserver = vi.fn().mockImplementation(() => ({ + observe: vi.fn(), + unobserve: vi.fn(), + disconnect: vi.fn(), +})) + +// Suppress console.error in tests (optional, remove if you want to see errors) +const originalError = console.error +beforeAll(() => { + console.error = (...args) => { + if ( + typeof args[0] === 'string' && + args[0].includes('Warning: ReactDOM.render is no longer supported') + ) { + return + } + originalError.call(console, ...args) + } +}) + +afterAll(() => { + console.error = originalError +}) \ No newline at end of file diff --git a/web/src/assets/styles/main.css b/web/src/assets/styles/main.css new file mode 100644 index 0000000..b14f9df --- /dev/null +++ b/web/src/assets/styles/main.css @@ -0,0 +1,161 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + :root { + --font-sans: 'Inter', system-ui, sans-serif; + --font-display: 'Cal Sans', 'Inter', system-ui, sans-serif; + --font-mono: 'JetBrains Mono', monospace; + } + + html { + scroll-behavior: smooth; + } + + body { + @apply font-sans text-dark-900 bg-white antialiased; + font-feature-settings: 'cv02', 'cv03', 'cv04', 'cv11'; + } + + ::selection { + @apply bg-brand-500/30 text-brand-900; + } + + ::-webkit-scrollbar { + width: 8px; + height: 8px; + } + + ::-webkit-scrollbar-track { + @apply bg-dark-100; + } + + ::-webkit-scrollbar-thumb { + @apply bg-brand-500/50 rounded-full hover:bg-brand-500; + } +} + +@layer components { + .container { + @apply max-w-7xl mx-auto px-4 sm:px-6 lg:px-8; + } + + .btn { + @apply inline-flex items-center justify-center gap-2 px-6 py-3 font-medium rounded-lg transition-all duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed; + } + + .btn-primary { + @apply btn bg-brand-500 text-white hover:bg-brand-600 focus-visible:ring-brand-500 shadow-lg shadow-brand-500/25; + } + + .btn-secondary { + @apply btn bg-dark-100 text-dark-900 hover:bg-dark-200 focus-visible:ring-dark-500 border border-dark-200; + } + + .btn-outline { + @apply btn border-2 border-brand-500 text-brand-500 hover:bg-brand-500 hover:text-white focus-visible:ring-brand-500; + } + + .btn-ghost { + @apply btn text-dark-600 hover:text-dark-900 hover:bg-dark-100 focus-visible:ring-dark-500; + } + + .card { + @apply bg-white rounded-2xl border border-dark-100 overflow-hidden transition-all duration-300 hover:shadow-xl hover:border-brand-200; + } + + .card-hover { + @apply card hover:-translate-y-1 hover:shadow-glow; + } + + .section { + @apply py-16 sm:py-24 lg:py-32; + } + + .section-title { + @apply text-3xl sm:text-4xl lg:text-5xl font-display font-bold text-dark-900 tracking-tight; + } + + .section-subtitle { + @apply mt-4 text-lg sm:text-xl text-dark-600 max-w-2xl; + } + + .badge { + @apply inline-flex items-center px-3 py-1 rounded-full text-sm font-medium; + } + + .badge-brand { + @apply badge bg-brand-100 text-brand-700; + } + + .badge-dark { + @apply badge bg-dark-100 text-dark-700; + } + + .input { + @apply w-full px-4 py-3 rounded-lg border border-dark-200 bg-white text-dark-900 placeholder-dark-400 transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-brand-500/20 focus:border-brand-500; + } + + .label { + @apply block text-sm font-medium text-dark-700 mb-1.5; + } + + .link { + @apply text-brand-500 hover:text-brand-600 underline-offset-2 hover:underline transition-colors; + } +} + +@layer utilities { + .text-balance { + text-wrap: balance; + } + + .animate-delay-100 { + animation-delay: 100ms; + } + + .animate-delay-200 { + animation-delay: 200ms; + } + + .animate-delay-300 { + animation-delay: 300ms; + } + + .animate-delay-400 { + animation-delay: 400ms; + } + + .animate-delay-500 { + animation-delay: 500ms; + } + + .gradient-text { + @apply bg-gradient-to-r from-brand-500 to-brand-700 bg-clip-text text-transparent; + } + + .glass { + @apply bg-white/80 backdrop-blur-lg border border-white/20; + } + + .glass-dark { + @apply bg-dark-900/80 backdrop-blur-lg border border-dark-700/50; + } +} + +@keyframes shimmer { + 0% { + background-position: -200% 0; + } + 100% { + background-position: 200% 0; + } +} + +.skeleton { + @apply animate-pulse bg-dark-200; + background: linear-gradient(90deg, #e2e8f0 25%, #f1f5f9 50%, #e2e8f0 75%); + background-size: 200% 100%; + animation: shimmer 1.5s infinite; +} \ No newline at end of file diff --git a/web/src/components/layout/Footer.tsx b/web/src/components/layout/Footer.tsx new file mode 100644 index 0000000..7d757ff --- /dev/null +++ b/web/src/components/layout/Footer.tsx @@ -0,0 +1,122 @@ +import { Link } from 'react-router-dom' +import { Code, Github, Linkedin, Twitter, Mail, MapPin, Phone } from 'lucide-react' + +const footerLinks = { + services: [ + { label: 'Web Development', href: '#services' }, + { label: 'Backend & APIs', href: '#services' }, + { label: 'Cloud & DevOps', href: '#services' }, + { label: 'Architecture', href: '#services' }, + { label: 'Modernization', href: '#services' }, + { label: 'Advisory', href: '#services' }, + ], + company: [ + { label: 'About', href: '#experience' }, + { label: 'Projects', href: '#projects' }, + { label: 'Skills', href: '#skills' }, + { label: 'Contact', href: '#contact' }, + ], + legal: [ + { label: 'Privacy Policy', href: '/privacy' }, + { label: 'Terms of Service', href: '/terms' }, + { label: 'Cookie Policy', href: '/cookies' }, + ], +} + +const socialLinks = [ + { icon: Github, href: 'https://github.com/urpi', label: 'GitHub' }, + { icon: Linkedin, href: 'https://linkedin.com/company/urpi', label: 'LinkedIn' }, + { icon: Twitter, href: 'https://twitter.com/urpi_be', label: 'Twitter' }, + { icon: Mail, href: 'mailto:contact@urpi.be', label: 'Email' }, +] + +export function Footer() { + return ( +
+
+
+
+ + + URPI + +

+ Web application & backend development experts. Building scalable, maintainable software solutions with Go, Java, React, and cloud-native technologies. +

+
+ {socialLinks.map(({ icon: Icon, href, label }) => ( + + + + ))} +
+
+ +
+

Services

+
    + {footerLinks.services.map((link) => ( +
  • + + {link.label} + +
  • + ))} +
+
+ +
+

Company

+ +
+ +
+

Contact

+
+
+ + Belgium +
+ + +
+
+
+ +
+

+ © {new Date().getFullYear()} URPI. All rights reserved. +

+
+ {footerLinks.legal.map((link) => ( + + {link.label} + + ))} +
+
+
+
+ ) +} \ No newline at end of file diff --git a/web/src/components/layout/Header.tsx b/web/src/components/layout/Header.tsx new file mode 100644 index 0000000..7cbdb42 --- /dev/null +++ b/web/src/components/layout/Header.tsx @@ -0,0 +1,109 @@ +import { useState, useEffect } from 'react' +import { Link } from 'react-router-dom' +import { Menu, X, Code } from 'lucide-react' +import { Button } from '../ui/Button' +import { cn } from '../../utils/helpers' + +const navItems = [ + { href: '#services', label: 'Services' }, + { href: '#projects', label: 'Projects' }, + { href: '#skills', label: 'Skills' }, + { href: '#experience', label: 'Experience' }, + { href: '#contact', label: 'Contact' }, +] + +export function Header() { + const [scrolled, setScrolled] = useState(false) + const [mobileMenuOpen, setMobileMenuOpen] = useState(false) + + useEffect(() => { + const handleScroll = () => setScrolled(window.scrollY > 20) + window.addEventListener('scroll', handleScroll) + return () => window.removeEventListener('scroll', handleScroll) + }, []) + + const scrollToSection = (href: string) => { + const sectionId = href.replace('#', '') + const element = document.getElementById(sectionId) + if (element) { + element.scrollIntoView({ behavior: 'smooth' }) + } + setMobileMenuOpen(false) + } + + return ( +
+ +
+ ) +} \ No newline at end of file diff --git a/web/src/components/sections/Contact.tsx b/web/src/components/sections/Contact.tsx new file mode 100644 index 0000000..6ad013d --- /dev/null +++ b/web/src/components/sections/Contact.tsx @@ -0,0 +1,217 @@ +import { useState } from 'react' +import { useApiMutation } from '../../hooks/useApi' +import { api } from '../../services/api' +import { type ContactForm } from '../../types' +import { Button } from '../ui/Button' +import { Input, Textarea } from '../ui/Input' +import { Card } from '../ui/Card' +import { Badge } from '../ui/Badge' +import { Mail, MapPin, Phone, Send, CheckCircle, AlertCircle, Loader2 } from 'lucide-react' +import { validateEmail } from '../../utils/helpers' + +export function Contact() { + const [formData, setFormData] = useState({ + name: '', + email: '', + subject: '', + message: '', + }) + const [errors, setErrors] = useState>({}) + const [submitStatus, setSubmitStatus] = useState<'idle' | 'success' | 'error'>('idle') + const [errorMessage, setErrorMessage] = useState('') + + const { mutate: submitContact, loading } = useApiMutation(api.contact.submit) + + const validateForm = (): boolean => { + const newErrors: Partial = {} + + if (!formData.name.trim() || formData.name.length < 2) { + newErrors.name = 'Name must be at least 2 characters' + } + + if (!validateEmail(formData.email)) { + newErrors.email = 'Please enter a valid email address' + } + + if (!formData.subject.trim() || formData.subject.length < 5) { + newErrors.subject = 'Subject must be at least 5 characters' + } + + if (!formData.message.trim() || formData.message.length < 10) { + newErrors.message = 'Message must be at least 10 characters' + } + + setErrors(newErrors) + return Object.keys(newErrors).length === 0 + } + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + + if (!validateForm()) return + + setSubmitStatus('idle') + setErrorMessage('') + + const result = await submitContact(formData) + + if (result) { + setSubmitStatus('success') + setFormData({ name: '', email: '', subject: '', message: '' }) + } else { + setSubmitStatus('error') + setErrorMessage('Failed to submit form. Please try again or email us directly.') + } + } + + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.target + setFormData(prev => ({ ...prev, [name]: value })) + if (errors[name as keyof ContactForm]) { + setErrors(prev => ({ ...prev, [name]: undefined })) + } + } + + const contactInfo = [ + { icon: Mail, label: 'Email', value: 'contact@urpi.be', href: 'mailto:contact@urpi.be' }, + { icon: MapPin, label: 'Location', value: 'Belgium', href: null }, + { icon: Phone, label: 'Phone', value: '+32 470 00 00 00', href: 'tel:+32470000000' }, + ] + + return ( +
+
+
+
+ Get In Touch +

Let's Build Something Great

+

+ Have a project in mind? Need technical guidance? Looking for a development partner? + Let's start a conversation. +

+ +
+ {contactInfo.map((item) => ( +
+
+ +
+
+

{item.label}

+ {item.href ? ( + + {item.value} + + ) : ( +

{item.value}

+ )} +
+
+ ))} +
+ +
+

Typical Response Time

+

We usually respond within 24 hours during business days.

+
+
+ +
+ + {submitStatus === 'success' ? ( +
+
+ +
+

Message Sent!

+

Thanks for reaching out. We'll get back to you within 24 hours.

+ +
+ ) : ( +
+ {submitStatus === 'error' && ( +
+ +

{errorMessage}

+
+ )} + + + + + + + +